LSM Compaction优先级参数compaction_pri详解:四种策略与调优实践
如果你的存储引擎是LSM树结构那你一定对compaction不陌生。但大部分人调参时都盯着write buffer、block cache、background threads这几个面很少有人真正关心compaction_pri这个参数。我之前就遇到过一台机器磁盘IO持续飙高应用延迟不断告警所有常规参数都试了一遍最后通过调整Compaction优先级才把问题压下去。今天就把这个东西彻底说清楚。compaction_pri决定的是LSM引擎在触发合并压缩时到底优先选择哪些SSTable文件来执行合并。它不像compaction_trigger那样直接决定“什么时候压缩”而是决定“从哪个文件开始压缩”。听起来只是个顺序问题但顺序差一点写放大、空间放大、读放大的表现会差一个数量级。这篇文章适合正在维护LSM存储引擎、或者长期和leveled compaction打交道的工程师。无论你是还没搞懂这个参数含义还是已经在生产环境被它坑过希望这篇能帮你把思路理顺。1. CompactionPri是什么凭什么影响全局性能1.1 先还原一个让IO飙升的真实场景某一天我接到线上告警一个数据服务节点的磁盘utilization长期在95%以上但CPU只有30%内存也不紧张。第一反应是磁盘要坏了但换到云盘还是这样。后来抓了IO轨迹发现大部分IO都是后台压缩任务产生的而不是业务读写。当时我就去查这条链路的compaction配置。引擎的自动压缩已经开启任务数也够但为什么压缩会占用这么多IO仔细观察后台任务日志发现压缩线程总是在合并那些最老、最大的文件导致每次合并都要读取和重写几十GB数据。而这些文件明明有很多“空洞”和过期数据压缩收益并不高。问题就出在compaction_pri上。因为该引擎默认使用的优先级策略会让压缩任务优先处理“补偿大小”最大的文件这个文件往往是最老的那一批大文件。在大文件上做反复压缩IO开销自然下不来。后来我把优先级策略改成按“最小重叠比”选择后IO降了一半以上。要说清楚为什么得先理解compaction队列的底层逻辑。1.2 Compaction排队的底层逻辑LSM引擎的地址空间被划分为多个层level新写入的数据先进内存表然后落盘到底层通常是L0。随着数据不断写入低层文件会向高层合并这个过程就是compaction。问题来了同一时刻可能有多个文件满足合并条件。比如L1层有5个文件其中3个与L2层文件有重叠另外2个只和底层文件有很轻的接触。这时候该合并哪一组如果随便选一组开始很可能造成两种后果选了与高层完全没重叠的文件合并时几乎不需要重写高层文件效果不错但优先级不高选了与高层大面积重叠的文件合并时要读入大量高层数据再把合并结果写回去写放大直接爆炸。compaction_pri就是干这个的决策者。它通过一个简单的评分规则给所有候选文件排序排序靠前的先被压缩线程处理。不同的评分规则本质上是对“我们当前最担心什么问题”的回答。担心空间占用膨胀就选能让文件数量下降最明显的担心数据读取延迟就选能让读路径最短的担心写入峰值IO就选能让当前合并代价最小的。这就解释了为什么调整这个参数能直接影响整个集群的IO表现。2. 四种CompactionPri策略的行为差异大多数开源LSM存储实现里compaction_pri有四种可选值。它们的名字可能在不同版本里略有差异但语义比较稳定。我直接按底层逻辑逐个拆解。2.1 默认的ByCompensatedSize这个策略是很多引擎的默认项。它的评分标准是“补偿大小”最高的文件优先。先解释补偿大小怎么算。每个文件的得分不仅看自身大小还看它底部层的条目数占比。公式大致是文件的大小乘以一个系数这个系数由文件中有多少条目已经合并到了更底层来决定。如果某个文件里大部分数据都已经“下沉”到了深层那么它在合并时被重写的概率更低得分会被放大于是更可能被优先选择。这个策略的目标就是最小化写放大。因为它倾向于先选那些已经部分合并、只需要做少量重写的文件整体重写代价最小。但代价也很明显如果一个老文件非常大即使它同时和下层多个文件重叠它也会因为“补偿后评分最高”被选中。结果就是压缩线程反复去啃这块难啃的硬骨头IO峰值居高不下。我遇到的那个问题就是这种策略的典型副作用。2.2 OldestLargestSeqFirst与OldestSmallestSeqFirst这两个策略一看名字就知道它们和“旧”和“序号”有关。LSM引擎会给每条写入分配一个递增序列号。文件越旧包含的序列号区间越靠前。OldestLargestSeqFirst的意思是优先选择最老文件里最大序列号最大的那个。换句话说它想尽快把最老的数据往下层推。因为最老的文件里往往包含大量已被覆盖的旧版本数据把这些数据尽快合并到下一层并清理掉能有效压缩空间占用。OldestSmallestSeqFirst则优先选择最老文件里最小序列号最小的文件。它关注的是“谁的历史包袱最重”把这个文件先处理掉相当于先把最底层的尘埃清理干净。这两个策略对空间放大敏感、且数据更新频繁的场景很有用。比如你在做时间窗口数据存储热点集中在近期老数据基本都是死数据那这个策略能快速腾出磁盘空间。不过要小心它们对写放大不太友好。因为老文件一旦和下层文件重叠范围大合并时重写代价会非常高。2.3 MinOverlappingRatio这个策略是我现在最常用的。它的评分标准是“文件与其目标层文件的重叠范围占比”。重叠比越小优先级越高。假设L1层有一个小文件它和L2层唯一一个文件只重叠10%的数据而旁边另一个大文件重叠了80%。选择前者合并只需要重写L2层那10%对应的数据块代价极小选择后者几乎要重写整个L2层文件。MinOverlappingRatio就是筛出这种“代价最小”的合并任务让后台压缩线程用最小的IO完成单位工作量。这个策略带来的直观效果是压缩任务的单次耗时显著降低后台IO变得平稳业务高峰期的读写抖动明显减少。但它也有短板。因为每次只挑代价最小的任务有些“难啃”的文件可能长期得不到合并导致这些文件里的过期数据一直占着空间。空间放大问题会比前几种策略更突出。好在实际运维中大部分业务对空间敏感度并不高IO平稳反而更值钱。三种策略的核心差异我用一个表来对照策略名优先处理对象主要目标副作用ByCompensatedSize补偿后体积最大的文件降低写放大大文件IO峰值高OldestLargestSeqFirst最老文件中最大Seq最大者降低空间放大单次压缩代价高OldestSmallestSeqFirst最老文件中最小Seq最小者降低空间放大同样可能高代价MinOverlappingRatio与下层重叠比最小的文件降低单次IO代价可能延迟清理垃圾3. 怎么判断你的引擎该用哪个CompactionPri3.1 按读写比例选型选择compaction_pri之前先问自己一个问题这台机器到底在为什么买单如果业务是写多读少核心诉求是写入延迟和IO峰值稳定那么MinOverlappingRatio通常是最优解。它能让压缩任务细碎且均匀不太会出现把磁盘打满的长尾压缩。如果业务是读多写少且大量读请求会穿透到较深层文件那么空间放大带来的影响会被放大。因为空间越冗余读路径上的无效SSTable越多每次读请求需要检查的文件数也随之增加。这时候OldestLargestSeqFirst这类策略更合适它能更快地把垃圾数据清出磁盘让读路径更短。如果业务是数据持续大量写入且磁盘空间经常告急ByCompensatedSize可能是个折中。它能保证压缩效率让每个压缩任务都“物有所值”但代价是IO峰值不可控。我自己的经验是在拿不准的时候可以先从MinOverlappingRatio试起。因为它对整体系统状态的干扰最小出现异常的几率最低。3.2 按放大因子选型读放大、写放大、空间放大三者之间存在跷跷板关系。compaction_pri的选择实际上就是你在三个放大因子之间做权衡。写放大最优的场景数据只是顺序写入永远不更新。这时候任何压缩策略成本都差不多因为不会有太多垃圾数据选ByCompensatedSize会更稳妥。空间放大最优的场景写入大量重复key但update和delete居多。此时老文件里塞满了被覆盖的旧值最急迫的任务是把这些无效数据清掉。OldestLargestSeqFirst和OldestSmallestSeqFirst都是好选择。读放大最优的场景每次查询都要走很长的层级链如果低层文件长期不整理读放大指数上涨。此时MinOverlappingRatio能保持每个层级数据紧凑因为小代价合并持续在发生文件的重叠关系不会恶化。3.3 一个简单的实验矩阵没有哪个参数是永恒最优的但只要你有实验条件就可以按下面的矩阵快速定位问题区间现象当前策略倾向建议尝试策略压缩线程长期满负荷IO尖峰ByCompensatedSize或Oldest系列MinOverlappingRatio磁盘空间快速膨胀delete后不回收MinOverlappingRatioOldestLargestSeqFirst读延迟p99高低层文件过多任意但文件增长快ByCompensatedSize写入延迟偶发抖动无规律OldestSmallestSeqFirstMinOverlappingRatio应用这个表的时候注意先抓监控指标压缩任务的单次耗时、每秒压缩字节数、后台压缩线程队列长度、层次文件数量变化。再根据指标落进对应的现象行有方向地进行AB对比。4. 实测记录调整CompactionPri前后的完整对比4.1 测试环境与基线为了让你有个直观感受我拿一个模拟项目X的存储层做了实验。环境是8核16G内存的虚拟机挂载一块SSD云盘。写入模型是持续插入带时间戳的事件数据每小时一个时间分区数据量总量约80GB。读模型是随机查询最近7天的数据同时有少量跨分区查询。初始配置里compaction_pri用的是默认的ByCompensatedSize后台压缩线程数设为4目标层文件大小16MBlevel0触发压缩文件数4。跑了一天后拿到基线数据写延迟p50大约1.2msp99约8.5ms磁盘utilization平均65%高峰期到92%后台压缩任务单次耗时最长的达到43秒L1层文件数量在700到900之间波动一直无法收敛。显然这个IO尖峰和文件数量波动是有强关联的。每次那个43秒的大任务一出现其他读写就明显卡顿。4.2 修改配置后的监控指标变化我把compaction_pri改成MinOverlappingRatio其他配置完全不动再跑同样的写入模型。第一天观察到的变化是写延迟p50降到0.9msp99降到6.1ms磁盘utilization平均值降到48%高峰期勉强到70%后台压缩任务单次耗时最长不超过6秒L1层文件数量从700到900的波动区间收敛到500到600。当时的压缩任务变得非常细碎每次干的活都不多但频率变高了。磁盘IO从“几次大爆发”变成了“持续的小浪花”。业务查询几乎感受不到压缩带来的影响。从写放大角度看总写入IO量其实没有明显减少但分布变得均匀。后台任务不再抢占前台IO峰值窗口这比单纯降低总量对用户体验更有意义。4.3 为什么结果和预期不完全一致有一点非常有意思我原本以为改成MinOverlappingRatio后空间回收速度会变差因为难清理的大文件可能被长期搁置。但实际观察两天后总磁盘占用反而比之前少了大概6%。原因在于旧策略虽然每个任务都抓大文件但大文件经过压缩后很快又会因为新数据写入而重新变大导致它反复成为候选。垃圾数据在这个循环里并没有被彻底清掉。而新策略让压缩任务更分散各层文件整体都保持紧凑垃圾数据反而能在日常的小任务中被碎片化清理。当然这也暴露了新策略的另一个边界当底层的某个大文件真的没有任何重叠文件可挑时它确实会被冷落一段时间。最终它会被下一轮任务选中但滞后期可能比旧策略长。如果你的业务对磁盘占用极度敏感不能接受这个滞后那就还是用Oldest系列更靠谱。5. 联动参数与常见误区5.1 必须一起调的三个邻居参数compaction_pri不是孤立生效的它和下面这几个参数强耦合。如果只改优先级不改它们效果很容易被稀释。第一个是max_background_jobs。优先级策略决定的是“选谁”而后台任务数决定的是“同时干几个”。如果你把任务数调得过高哪怕选了一个低代价任务也会出现多个压缩任务并发抢占IO。通常我会先把任务数调低再观察单个压缩任务的耗时分布否则难以判断策略本身的优劣。第二个是level0_file_num_compaction_trigger。L0层文件一旦堆积到阈值就会引发紧急压缩。这个紧急压缩是不完全遵循优先级筛选的它会直接压L0层所有文件。这意味着如果你的L0触发阈值设得太低那么大部分压缩机会会被L0紧急合并占走compaction_pri的作用空间就被挤占了。想让策略真正发挥作用我建议把这个触发值稍微调大让L0层先积攒一些文件再统一进入带优先级的调度逻辑。第三个是target_file_size_base和max_bytes_for_level_base。这两个参数决定了一层文件的目标大小和总容量。它们与优先级策略配合的逻辑是文件越小重叠关系越容易判断MinOverlappingRatio的优势越明显。文件越大单个任务的代价就高更依赖Oldest系列来找“大而旧”的文件处理。5.2 三个容易让人踩坑的认识误区误区一认为compaction_pri只是性能微调参数改不改无所谓。实际上在数据量超过单机几十GB后这个参数直接决定后台IO的分布形态。在最坏情况下错误策略会让压缩任务长期聚焦在几个大文件上造成持续的IO饥饿。误区二以为MinOverlappingRatio永远是最优解。它确实很稳但如果你面临的是磁盘空间即将写满的极端状况它反而会让空间问题恶化。此时需要迅速切换到Oldest系列把最老的垃圾数据尽快清理掉。选策略要跟业务阶段匹配而不是跟风。误区三只改参数不观察监控。我见过不少同行把compaction_pri改成自己认为对的值然后发现性能没变化就开始怀疑参数没用。实际上如果没有单独记录“压缩任务平均耗时”和“后台压缩线程队列长度”你就看不到优先级策略带来的核心差异。这两个指标比单纯看写延迟更直观因为业务延迟有很多干扰因素。5.3 用最终一致性心态看待调优调优LSM引擎和调优传统数据库不太一样。传统数据库的很多参数改完立刻生效而compaction相关参数必须在数据写入累积、压缩任务持续运行之后才能体现完整效果。持久化运行的压缩任务本身就是一种“异步收敛”过程。所以我每次调完compaction_pri都不会急着下结论。至少要让它连续跑三个完整的compact周期再回头对比监控曲线。数据少、时间短很容易把偶然的IO平稳当成策略的胜利。还有个小技巧在生产环境验证之前先把策略改到自己本地测试实例上用同样的写入模型跑一遍记录压缩任务的大小分布。如果本地能看到明显的“大任务分成小任务”或“文件数量收敛”的信号再到生产环境改这样能省掉很多反复横跳的成本。聊到这里CompactionPri那些门道基本就都摊开了。每次折腾存储引擎调优我都会提醒自己先想清楚业务最怕什么再看参数该怎么侧写。没有绝对正确的最优值只有适合你当前工作负载和硬件形态的合理组合。按这个思路去调至少不会把方向跑偏。