资讯动态

RocksDB CompactionPri调优:四种优先级策略与场景选型

发布时间:2026/10/20 9:49:20 来源:尧图企业网站定制
RocksDB调优调到后面百分之八九十的人都会盯上compaction_pri这个参数。全称Compaction Priority说白了就是候选的压缩任务不止一个时先压谁、后压谁的排序规则。别小看这个排序它直接决定了一轮Compaction里文件被选中、被合并的路径进而改写整个DB的写放大、空间放大和读性能曲线。很多线上事故表面看是磁盘暴涨或者写入卡顿根子其实是这个不起眼的优先级选错了策略。这个参数适合谁来调凡是底层用了RocksDB、并且已经经历过“默认参数能跑但一上量就露馅”的团队都需要把CompactionPri纳入调优清单。它不像write_buffer_size那样直观也不像max_background_jobs那样立竿见影但它决定了你压下去的每一轮后台IO到底是不是花在了刀刃上。我今天就把这个参数从原理到实操完整聊一遍重点讲清楚四种枚举值各自适用什么场景、怎么配置、怎么观察效果以及我在真实项目里踩过的坑。1. 先搞明白Compaction Priority在解决什么问题1.1 LSM-Tree里永远存在的“候选者冲突”RocksDB底层是LSM-Tree结构数据先写MemTable刷盘成SST文件逐层向下合并。每个层级到了一定阈值就会触发Compaction但触发之后随之而来的问题是这一层里有好几个文件都满足合并条件合并不是流水线式的排队而是需要一个明确的“插队规则”——谁先被处理谁继续等着。这个规则就是Compaction Priority。简单说它作用的位置是CompactionPicker内部也就是负责“挑选本次合并哪些文件”的那个模块。你可以把它理解为排队叫号系统里的优先级排序如果只是“先到先得”结果可能很糟糕因为最先到达的文件未必对整体结构最有利。没有合理优先级时会出现什么现象最常见的是写放大飙升。假设L1层有十个文件其中五个和L2层几乎没有重叠另外五个和L2层大范围重叠。如果系统优先合并的是后面五个那么每一轮合并都要携带大量冗余数据下沉磁盘IO消耗成倍增加但又没有换来读性能的明显收益。选文件的顺序不对等于同样一份数据反复被搬来搬去这就是我在生产环境里见过最典型的隐性浪费。1.2 CompactionPri不是触发条件而是排序规则很多刚接触RocksDB的人会把compaction_pri和“什么时候触发Compaction”混为一谈。其实触发条件和它是两码事什么时候触发由level0_file_num_compaction_trigger、max_bytes_for_level_base这些参数决定一旦触发条件满足、进入候选队列接下来才是compaction_pri发挥作用。这个区别很重要因为它决定了你调参的心态。改compaction_pri不会让Compaction变得更频繁或更稀疏它只是改变“在已经决定要做合并的情况下优先处理谁”。所以如果线上Compaction积压严重第一反应应该是去看触发阈值和后台线程数而不是试图靠改优先级来“急救”。不过优先级选对了确实能明显缓解积压。为什么因为Compaction积压的本质是写放大过大、有效产出比太低大量IO被浪费在合并重叠度很高的文件上。选一个合适的优先级让每一轮合并都在“以最小的付出换取最大的结构收益”积压自然就降下来了。2. 四种CompactionPri策略的工作原理与选型逻辑2.1 kByCompensatedSize看着最“均衡”的默认策略kByCompensatedSize是早期版本的默认策略全称是按“补偿后大小”排序。这个名字很容易让人误解它不是简单按文件大小从大到小排。它的核心逻辑是每个文件有一个“补偿大小”这个值会综合考虑文件本身的大小以及它在整个层级中的影响范围。排序时优先选择补偿后大小最小的文件进行合并。为什么是“补偿后大小”因为一个文件虽然本身只有10MB但它可能和下一层有大片重叠合并时实际要读写的IO远超10MB另一个文件本身有50MB但和下一层完全不重叠合并时几乎就是文件搬迁付出的小。所以只看原始文件大小判断优先级是不科学的必须引入“补偿”概念把重叠范围、层级影响折算进来。不过这个策略有个典型问题它倾向于优先压缩“看起来很小但实际影响不小”的文件导致某些小文件可能会被反复选中。每次刚合并完它的体量变小了结果下一轮又变成“补偿后最小”的候选者再次被优先处理。我在一个模拟项目X里就遇到过这种情况写入量不大但Compaction线程却忙个不停打开rocksdb.cfstats一看合并的总字节数远超实际写入字节数写放大已经高到了令人头皮发麻的程度。2.2 kOldestLargestSeqFirst让最老的数据先沉到底kOldestLargestSeqFirst的策略偏向于时间维度。它会优先选择包含最老Sequence Number、并且文件尺寸也更大的那些文件。你可以把这个行为理解成“老数据优先下葬”越老的数据越应该早一点被合并到更深层去这样查询老数据时路径更短同时也能更早地释放上层的空间。这个策略和Tiered Compaction搭配得非常好。Tiered模式下各个层级更像是“暂存区”数据先快速写入多层由后台慢慢整理此时如果优先处理最老且最大的文件可以加速整个SST的整体下沉避免上层文件堆太久导致空间放大失控。我在日志类存储场景里用过这个组合效果立竿见影——磁盘占用增速从每天一个多GB降到几百MB后台IO也更平稳了。但它也有代价这种策略对写放大的控制不如后两种那么精细化。它是“时间优先”而非“成本优先”所以适合那些对磁盘空间更敏感、对写放大相对宽容的业务。典型场景是流水型数据的长期存储数据一旦落盘基本不会再更新重点是尽快把老数据组织好、省空间。2.3 kOldestSmallestSeqFirstTiered模式下的默认强者kOldestSmallestSeqFirst和上面那个只差一个词但行为差异明显。它会优先选择最老Sequence Number的、文件尺寸偏小的文件进行合并。这个策略的出发点很朴素先把最老的数据处理掉而且优先处理小文件合并的代价相对小整体IO更平滑。为什么Tiered Compaction常常默认或者推荐用这个因为Tiered模式下一个文件可能被多个层级“引用”文件之间的重叠关系不像Leveled那么直接。选最老且最小的文件去做合并IO成本可控同时又能保证老数据不断向深层推进。我个人的观察是这种策略在大量小文件持续写入的场景下对稳定性的贡献最明显Compaction峰值不会突然冲得特别高而是维持在一个可以接受的范围内。不过如果某个文件的Key Range恰好和很多文件重叠哪怕它是最老的、最小的合并它时依然可能拖出一大片IO。所以kOldestSmallestSeqFirst更像是一种“基准线策略”——在多数情况下表现良好但遇到极端重叠时会有些吃力。2.4 kMinOverlappingRatioLeveled模式下的写放大杀手kMinOverlappingRatio是我在Leveled Compaction下最常用的策略也是目前RocksDB官方比较推荐的思路之一。它的排序依据是文件和下一层的重叠比率重叠率低的文件优先被处理。重叠率怎么理解L1层的一个文件如果要合并进L2它可能需要覆盖L2层的好几个文件。如果只覆盖一个文件那这次合并只需要读少量数据、写少量数据代价就很小如果覆盖了七八个文件这次合并要带着大量无关数据一起重写代价就很大。kMinOverlappingRatio要做的事情就是每次都挑代价最小的那个文件先做把昂贵的合并留到后面。这个策略为什么对写放大如此友好因为Compaction的写放大和文件之间的重叠程度强相关。人为贪心地按“成本最小”处理一轮轮下来平均写放大会明显下降。我曾在测试环境做过对照组同样写入500GB数据用kByCompensatedSize时累计合并写入量接近1.2TB切换到kMinOverlappingRatio之后合并写入量降到700GB左右。这个差距让我后来在几乎所有Leveled场景下都选择了这个枚举值。不过要注意kMinOverlappingRatio对L0到L1的合并效果有限因为L0文件之间的Key Range本身就可能重叠严重上一层的“重叠比率”失真。但RocksDB的CompactionPicker对L0合并有自己的内部处理逻辑不完全依赖这个优先级所以实际使用时不需要太担心这一点。3. 实操配置从读取默认值到动态切换3.1 查看当前CompactionPri状态动手改参数之前先要知道当前数据库到底用的什么策略。RocksDB不像MySQL的show variables那么直接获取配置需要走GetOptions路径。在C里可以这样#include rocksdb/options.h #include rocksdb/db.h rocksdb::ColumnFamilyOptions cf_opts; db-GetOptions(column_family_handle, cf_opts); std::cout compaction_pri: static_castint(cf_opts.compaction_pri) std::endl;枚举值对应的数字是kByCompensatedSize为0、kOldestLargestSeqFirst为1、kOldestSmallestSeqFirst为2、kMinOverlappingRatio为3。拿到数字就知道线上跑的是哪个策略了。Java端更简单直接调用getColumnFamilyOptions()然后读取compactionPriority()即可。没有直接改过配置的存量库大概率是0也就是kByCompensatedSize。如果数据库是从某个新版本初始化且明确指定了Compaction Style默认值可能随版本变化所以不要想当然先查再说。3.2 静态配置与动态SetOptions两种方式配置CompactionPri有两种方式。第一种是初始化时在ColumnFamilyOptions里写好rocksdb::ColumnFamilyOptions cf_opts; cf_opts.compaction_pri rocksdb::CompactionPri::kMinOverlappingRatio; rocksdb::DB* db; rocksdb::ColumnFamilyHandle* cf_handle; rocksdb::DB::Open(rocksdb::DBOptions(), /data/rocksdb, {rocksdb::ColumnFamilyDescriptor(default, cf_opts)}, cf_handle, db);Java端的写法类似ColumnFamilyOptions cfOpts new ColumnFamilyOptions() .setCompactionPriority(CompactionPriority.KMinOverlappingRatio);更实用的是第二种线上数据库已经在运行不可能重启这时用动态SetOptions调整# 通过RocksDB的SetOptions接口动态更新 # 注意参数名是字符串形式枚举名的写法不同语言略有差异Java端动态调整我常用这种方式import org.rocksdb.RocksDB; import org.rocksdb.ColumnFamilyHandle; import java.util.Collections; RocksDB db RocksDB.open(/data/rocksdb); ColumnFamilyHandle cf db.getDefaultColumnFamily(); db.setOptions(cf, Collections.singletonMap(compaction_pri, kMinOverlappingRatio));这里特别注意动态设置是作用于单个ColumnFamily的不是全局的。如果一个DB开了多个CF只设置了default其他CF还是老策略。我曾经因为漏看了这个细节以为已经把业务热点CF调过去了结果排查半天发现调的是另一个无关CF。多CF场景尽量遍历所有handle统一设置。3.3 设置后必须观察的阈值与配套参数compaction_pri不是孤立生效的它和几个兄弟参数配合才能发挥出理想效果。我每次调它的时候都会顺手检查下面这几个值首先是compaction_style。kMinOverlappingRatio在kCompactionStyleLevel下是主力推荐但在kCompactionStyleUniversal下就未必合适反过来kOldestSmallestSeqFirst在Tiered模式下更常用。如果你用的是Universal Compaction但还挂着Leveled思路的优先级等于穿错鞋跑步越跑越别扭。其次是level0_file_num_compaction_trigger。这个参数决定了L0同时有多少文件时会触发Compaction。优先级再合理如果L0堆积的触发阈值设置过高前台写入仍然会因为L0文件过多而卡顿。我一般会把L0触发阈值和Compaction后台线程数放在一起调确保Compaction队列不会长期处于饱和状态。最后是max_background_jobs。再好的优先级排序如果后台线程不够积压还是积压。在调整compaction_pri之前先确认max_background_jobs和max_background_compactions没有明显偏小否则你很难说清效果到底是优先级带来的还是线程数变化带来的。控制变量调参的黄金法则。4. 真实场景调优三个案例复盘4.1 案例一写入吞吐上不去CPU全耗在Compaction上这个案例来自某跨平台系统的存储模块底层用RocksDB做元数据存储写入量不算极端但吞吐始终上不去。查看监控时发现CPU使用率高的时间段和Compaction线程活跃时间段高度重合rocksdb.cfstats里显示合并写入量是实际写入量的好几倍。诊断过程很简单先看Compaction StyleLeveled再看compaction_pri默认的kByCompensatedSize。于是我做了一次切换把优先级换成kMinOverlappingRatio同时保持其他参数不变。观察了半天后写放大数据开始下降CPU的Compaction占用曲线也从满负荷变成了有规律的脉冲。事后复盘原因这个业务的Key写入有明显热点区间新写入的文件和下一层重叠率普遍较高。kByCompensatedSize按补偿大小排序优先处理的往往是一些新生成的、和下一层大面积重叠的文件导致每一轮Compaction都像在搬一座山IO付出极大。kMinOverlappingRatio把重叠率低的文件优先处理一轮轮下来成本被摊薄了整体写放大自然就下去了。4.2 案例二磁盘空间持续暴涨查了一圈发现是老数据在反复横跳另一个案例是某日志型存储服务数据大部分是只追加写入很少有更新删除。服务跑了两三周后磁盘占用比预估高出一大截DBA第一反应是数据量涨了但查了实际写入量后对不上怀疑是RocksDB空间放大异常。我用RocksDB自带的rocksdb.levelstats看了各层文件大小分布发现L1层文件数量异常多而且总大小明显超过该层的容量上限。继续翻LOG文件发现L1到L2的Compaction频繁选中的文件居然都是同一批近期文件。这个表现很符合kByCompensatedSize的特征刚合并完的文件变“小”了又被当成补偿后最小的候选者反复被拖去合并。处理方式把这个CF的存储模式调整为更适合日志追加场景的Tiered Compaction同时把compaction_pri改为kOldestSmallestSeqFirst。调整后老数据开始稳步下沉到深层顶层文件不再被反复折腾磁盘占用的增长曲线迅速回归到和实际写入量相匹配的水平。那次之后我对“数据是只增不改”的业务默认都会考虑Tiered加上时间维度的优先级。4.3 案例三读延迟偶发飙升Compaction和查询相互踩踏第三个案例是读多写少的服务写入量不大但时不时会有一次明显的读延迟尖刺。排查时定位到Compaction任务在尖刺时间点前后被触发而且Compaction选中处理的是一个很大、和下层重叠很多的老文件长时间占用IO。这类场景的痛点是一次重Compaction成本太高长时间占用后台线程和磁盘带宽导致前台读请求的延迟被拉高。用kMinOverlappingRatio之后重成本的Compaction被拆散成多次低成本任务虽然总耗时可能更长但每次占用的资源有限前台读的尖刺明显消失。这也是我在实际项目里最推崇kMinOverlappingRatio的原因它不是最快的但它是“平滑的”。对在线服务来说一次100毫秒的重合并可能比十次10毫秒的轻合并更危险前者会让尾延迟爆炸后者却可以完全被缓冲掉。选择优先级本质上是选择系统的抖动特性。5. 踩坑记录与排查清单5.1 设置的优先级一直没生效这是最常见的困惑。排查看三点第一动态SetOptions是否作用到了正确的ColumnFamily第二是否设置了但没有等当前正在进行的Compaction批次结束第三你确认数值已经写入但行为没变化可能是对这个优先级的作用范围理解有偏差——它只影响新挑选的Compaction任务已经在队列里的老任务不会因为参数改了而中止重排。有一个陷阱很隐蔽如果设置了kMinOverlappingRatio但观察时间太短可能刚好赶上L0到L1的批量合并阶段这个阶段有些行为不完全受该优先级控制。建议观察至少一个完整的Compaction周期也就是看到L1及以下层级发生多次合并之后再做结论。5.2 优先级和Compaction Style不匹配我在前面的案例里提到过kMinOverlappingRatio和Leveled是天作之合但如果你在Universal/Tiered模式下强行使用重叠率逻辑效果会非常差。反过来倒也不是说时间维度的优先级不能在Leveled下用只是它容易让“重叠率很高但很老”的文件拖拽大量IO写放大控制明显不如重叠率策略。建议对照表如下策略推荐Compaction Style核心优势典型代价kByCompensatedSize通用默认均衡、适应面广产物文件易被反复选中写放大偏高kOldestLargestSeqFirstUniversal / Tiered老数据快速下沉空间控制好对大文件合并成本预估不细kOldestSmallestSeqFirstUniversal / Tiered平滑稳定IO脉冲低极端重叠场景会有一次重代价kMinOverlappingRatioLeveled写放大最低抖动小对L0场景帮助有限5.3 不要只调这一个参数最后一条血泪经验compaction_pri是锦上添花的参数不是包治百病的偏方。你在线上看到的Compaction问题往往是多因素叠加的结果MemTable刷盘太快、L0触发阈值太低、后台线程数不足、目标文件大小不合理任何一个都可能让Compaction表现变差。我的习惯是先把write_buffer_size、level0_file_num_compaction_trigger、max_background_jobs这些基础参数调到一个相对合理的范围再通过compaction_pri做细粒度的“选择性优化”。否则基础都歪了优先级配得再完美也是白搭。而且每次改动只动一个变量记录切换前后一周内的写放大、空间放大、P99读延迟数据用数据说话远比凭感觉调要靠谱得多。CompactionPri这个参数就像一把精细的手术刀用对了地方能精准解决写放大和抖动问题用错了方向反而会让系统行为变得难以解释。在真实项目的调优过程中我一直坚持“先看懂LSM结构再决定优先级”的顺序先确认业务是写多读少、读多写少还是只增不改再针对LSM当前的结构瓶颈去匹配策略。实际操作里kMinOverlappingRatio在大多数Leveled场景下都有着亮眼表现而Tiered场景我则更倾心于基于序列号的两个时间维度策略。如果你还在用默认值跑业务不妨花一个Compaction周期做个对照测试多半会有意外收获。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑