RocksDB Leveled Compaction:层级不变式与 CompactionPicker

原文发布于 quant67.com,转载请保留出处。

第 6 篇VersionSet::LogAndApply 把 flush 产出的 SST 挂进 LSM 快照;第 9 篇 说明 L0 文件多时 读放大 如何上升。Leveled Compaction 是 RocksDB 默认策略(CompactionStyle::kCompactionStyleLevel):后台 CompactionJobLevelCompactionPicker 选定的输入集上归并,把数据逐层下推,同时尽量维持 L1 及以下 key 范围不重叠 的不变式。

本文是系列 第 10 篇 ,源码锚点:RocksDB 9.xdb/version_set.ccComputeCompactionScoreCalculateBaseBytes)、db/compaction/compaction_picker_level.ccdb/compaction/compaction_picker.h写放大(WA)、读放大(RA)、空间放大(SA) 的定义与 Leveled 近似公式见 storage/31 LSM 工程调优------本篇只写 RocksDB 内核如何维护层级结构与触发 compaction,不重复该文推导表格。

版本锚定 :RocksDB 9.xfacebook/rocksdb v9.10.0 及相邻 release)。实验:reproduce/run_db_bench.sh exp10fillrandom,stats)。


一、Leveled 要解决什么问题

MemTable flush 持续向 L0 追加 SST。若不做 compaction:

  • L0 文件 key 范围可重叠 ,点查需扫描多个文件(RA 上升,见 storage/31 第二节)。
  • 多版本与 tombstone 无法及时归并(SA 上升)。
  • 旧文件长期不被重写,但新数据仍反复 flush + 未来合并(WA 在「从不 compact」与「每层 T 倍合并」之间取平衡)。

Leveled 的核心契约:

层级 SST 间 key 范围 容量控制方式
L0 可重叠 文件 个数level0_file_num_compaction_trigger)+ 总字节与 Lbase 比较
L1 ... Ln-1 互不重叠 层总字节上限 MaxBytesForLevel(i)
最底层 不重叠 通常不再向下 compact(除非 TTL / 手动)

L1+ 不重叠使 Version::Get 每层 二分定位至多一个 SST第 7 篇);L0 是读路径上 唯一可能多路扫描的层


二、Size Ratio 与 MaxBytesForLevel

静态配置下(level_compaction_dynamic_level_bytes = false),VersionStorageInfo::CalculateBaseBytesdb/version_set.cc)为每层填充 level_max_bytes_

  • L1 上限 = max_bytes_for_level_base(默认 256 MiB 量级,可调)。
  • Li( i≥2i \ge 2 i≥2)上限 = Li−1 ×T×MaxBytesMultiplerAdditional(i−1) L_{i-1} \times T \times \text{MaxBytesMultiplerAdditional}(i-1) Li−1×T×MaxBytesMultiplerAdditional(i−1),其中 TT T = max_bytes_for_level_multiplier(默认 10)。

这与 storage/31 式 WAleveled≈T×(h−1) WA_{\text{leveled}} \approx T \times (h-1) WAleveled≈T×(h−1) 的假设一致:每层向下 compact 时,约需与 整层 L(i+1) 中 key 范围重叠的 ~ TT T 个文件做归并,用户字节在磁盘上被重写约 TT T 次/层。

动态层级字节(level_compaction_dynamic_level_bytes = true

大 CF 常用 dynamic level bytes :根据 实际数据分布 计算 base_level_ (L0 直接 compact 的目标层),使最底层承载绝大部分数据,上层可保持较空。CalculateBaseBytes 会:

  1. 找到 第一个非空层最大层 的实际大小;
  2. base_level_ 满足「Lbase 目标大小 ≈ max_bytes_for_level_base」;
  3. 向上/向下推导各层 level_max_bytes_

LevelCompactionBuilder::SetupInitialFiles 中,L0 的输出层为 vstorage_->base_level() 而非固定 L1------这是 9.x 与早期 LevelDB「L0 只进 L1」的重要差异。


三、L0 重叠:读代价与 compaction 输入

L0 文件来自 独立 flush不做 key 范围互斥。因此:

  • Version::Get 对 L0 按 从新到旧 遍历候选文件(可能全部查 Bloom + Index)。
  • 写(compaction) :L0→Lbase 一次 job 常合并 多个 L0 文件 + Lbase 中 范围重叠 的 Lbase 文件。

LevelCompactionPicker 在 L0 score 过高但 已有 L0→Lbase job 在进行 时,可改选 Intra-L0 compactionPickIntraL0Compaction / PickSizeBasedIntraL0Compaction):仅在 L0 内部归并,降低 sorted run 数,缓解 write stall ,且与 L0→Lbase 可并行(注释:Intra-L0 compaction is independent...)。

L0 trivial moveTryPickL0TrivialMove):若单个 L0 文件 key 范围 完全落在 Lbase 某文件间隙中、归并无实质 key 重叠,可直接 改层 而不重写数据------这是 WA 上的重要捷径。


四、Compaction Score 与触发条件

VersionStorageInfo::ComputeCompactionScoredb/version_set.cc)为每层计算 scorescore ≥ 1 表示该层需要 compaction(与历史「字节/目标比」语义兼容,9.x 对大 score 还有 10× 缩放 用于优先级,阈值仍为 1.0)。

L0 score

对 Leveled / Universal / FIFO,L0 基础 score 为:
score0=num_sorted_runslevel0_file_num_compaction_trigger \text{score}_0 = \frac{\text{num\_sorted\_runs}}{\texttt{level0\_file\_num\_compaction\_trigger}} score0=level0_file_num_compaction_triggernum_sorted_runs

Leveled 额外考虑 L0 总 compensated 大小 与 Lbase 上限关系,避免 L0 单文件过大导致后续 巨型 L0→Lbase(见源码注释:平衡 L0 文件数与字节)。

4.1 从 score 到 write stall 的决策直觉

Compaction scorewrite stall 是同一债务链上的两环:score 驱动后台 CompactionJob 调度;当前台 flush 快于 compaction 消化时,L0 文件数 (Leveled 下即 score0 \text{score}_0 score0 的分子)先触达 stall 阈值,不等 score 逻辑走完才停写。

SanitizeOptions 强制单调关系(A 级源码 options/cf_options.cc):
level0_file_num_compaction_trigger  ≤  level0_slowdown_writes_trigger  ≤  level0_stop_writes_trigger\texttt{level0\_file\_num\_compaction\_trigger} \;\le\; \texttt{level0\_slowdown\_writes\_trigger} \;\le\; \texttt{level0\_stop\_writes\_trigger} level0_file_num_compaction_trigger≤level0_slowdown_writes_trigger≤level0_stop_writes_trigger

阶段 典型条件(Leveled) 内核动作 读者该看什么
正常 L0 文件数 << < slowdown 无 stall rocksdb.stats 各层文件数
Compaction 欠债 L0 score ≥1\ge 1 ≥1 调度 L0→Lbase / Intra-L0 compaction-pendingnum-running-compactions
Delay L0 数 ≥\ge level0_slowdown_writes_trigger WriteController 限速 sleep LOG Stalling writesactual-delayed-write-rate
Stop L0 数 ≥\ge level0_stop_writes_trigger 写线程 bg_cv_.Wait() is-write-stopped=trueSTALL_MICROS

决策口诀trigger 是「该 compact 了」;slowdown 是「先减速给后台时间」;stop 是「硬等 compaction 清债」。Intra-L0(第三节)在 已有 L0→Lbase job 时降低 sorted run 数,争取 在 stop 之前 把 L0 拉回 delay 区------这是 Leveled 专有的「抢跑」手段,Universal 的 run 计数语义不同(第 11 篇 §二)。

本系列不写 stall 延迟毫秒数 :exp10 短跑 Write Stall (count): 0(第八节);稳定观察 stall 须延长负载或限速磁盘------数值以本机 db_bench --statistics=1 为准,不编造。

L1+ score

scorei= ∑file_compensated_size(未在 compact 中)MaxBytesForLevel(i) \text{score}_i = \frac{\sum \text{file\_compensated\_size(未在 compact 中)}}{\texttt{MaxBytesForLevel}(i)} scorei=MaxBytesForLevel(i)∑file_compensated_size(未在 compact 中)

LevelCompactionPicker::NeedsCompaction 扫描各层 score ≥ 1,或存在 TTL / periodic / bottommost 标记文件等,即返回 true。


五、LevelCompactionPicker 选 compaction 流程

LevelCompactionPickerdb/compaction/compaction_picker_level.cc)通过内部 LevelCompactionBuilder 分步构造 Compaction 对象:

flowchart TB NEED[&#34;NeedsCompaction?&#34;] --> BUILD[&#34;LevelCompactionBuilder::PickCompaction&#34;] BUILD --> INIT[&#34;SetupInitialFiles<br/>scan score >= 1&#34;] INIT --> L0{&#34;start_level == 0?&#34;} L0 -->|yes| OTHER[&#34;SetupOtherL0FilesIfNeeded&#34;] L0 -->|no| EXPAND[&#34;SetupOtherInputsIfNeeded&#34;] OTHER --> EXPAND EXPAND --> JOB[&#34;CompactionJob::Prepare/Run/Install&#34;]

要点:

  1. SetupInitialFiles :从 最高 score 层 选起始文件;L0 输出到 base_level() ,Li( i>0i>0 i>0)输出到 i+1
  2. ExpandInputsToCleanCut :扩展输入边界,保证 无遗漏 overlap 文件 ,且 L1+ 输入在 key 范围上形成 clean cut
  3. 并发约束 :L0 同时只允许 有限 L0→Lbase job(level0_compactions_in_progress() 非空时 PickFileToCompact 跳过新的 L0→Lbase)。
  4. CompactionReason :L0 文件数触发为 kLevelL0FilesNum;L1+ 字节超限为 kLevelMaxLevelSize(见 SetupInitialFiles 内赋值)。

Compaction 优先级与 round-robin

CompactionOptionscompaction_pri (如 kMinOverlappingRatiokRoundRobin)影响 同层多文件 时先 pick 哪一个。Round-robin 可一次 pick 连续多个 L0 文件 组成大 compaction(SetupOtherFilesWithRoundRobinExpansion),提高单次 job 吞掉 L0 debt 的效率,但单次 job CPU / 磁盘峰值更高------与第 12 篇 max_subcompactions 配合调优。

Picker 接口在 db/compaction/compaction_picker.h;CF 构造时按 compaction_style 实例化(ColumnFamilyDataLevelCompactionPicker / UniversalCompactionPicker / FIFOCompactionPicker)。


六、文献中的 Leveled 模型与 Tiering 争论

RocksDB 的 LevelCompactionPicker 实现的是 工业 leveled compaction (源自 LevelDB),不是 1996 年 LSM 论文里的唯一形态。读源码时应与下列 A 级论文 对照------它们定义 size ratio TT T、sorted run 数与 WAWA WA/ RARA RA 的定量争论。

6.1 代表 work 对照

工作 venue 优化目标 与 RocksDB 默认 leveled 的关系
O'Neil et al., LSM-Tree Acta Informatica 1996 顺序写 + 多级归并 概念起源
Dayan & Idreos, Merge Bush SIGMOD 2017 Tiering:同层多 run,降低 WAWA WA 对应 Universal 的理论端(第 11 篇
Dayan & Idreos, Dostoevsky SIGMOD 2018 在 leveled/tiering 间插值 解释 TT T 与层数 是旋钮;非 magic number
LevelDB / RocksDB leveled 工程 L0 重叠、L1+ 不重叠 本篇拆解对象

Dostoevsky 的核心结论(简述,细节见原文):在 固定写放大预算 下,纯 leveled 与纯 tiering 都不是全局最优;RocksDB 的 dynamic level bytesUniversal 是工程侧对同一 trade-off 的响应,不是论文算法的逐行移植。

6.2 假设---结论链

假设 若成立 若失效(生产常见)
L0 文件数低于 trigger RA 低、少 stall 写突发 → 第 11 篇
T=10T=10 T=10 且 key 均匀 WA≈T×(h−1)WA \approx T \times (h-1) WA≈T×(h−1)(storage/31 倾斜 key → 巨型 merge
Compaction 跟得上 flush stats 见稳态层级 短 benchmark 可能 0 次 compact(见第八节实测)

6.3 开放问题

  1. Workload-aware picker :learned / adaptive compaction 在研究与 RocksDB 实验特性中均有线索,无统一 GA 默认
  2. Remote compaction :存储分离后 leveled 的 网络读放大 是否抵消 WAWA WA 收益------须分论文与产品 release 引用。
  3. Intra-L0 最优性:何时优于 L0→Lbase 仍靠 trace/stats,缺 closed-form 规则。

七、与三种放大的衔接(不重复 storage/31 全文)

机制 主要影响的放大 内核落点
L0 文件数 / Intra-L0 RA(L0 扫描次数) ComputeCompactionScore、Intra-L0 picker
Size ratio TT T、层数 hh h WA MaxBytesForLevelmax_bytes_for_level_multiplier
不重叠 + 归并 tombstone SA CompactionIterator 丢弃旧版本(第 8 篇归并语义)
base_level 动态调整 WA / RA 折中 CalculateBaseBytes

调参 cookbook(200+ 项)见 storage/32 RocksDB 实践write stall 在 L0 堆积时由 第 11 篇 展开。


八、实验 exp10:观察 Leveled 层级分布

reproduce/run_db_bench.sh exp10 在 Leveled(--compaction_style=0)下跑 fillrandom + stats

bash 复制代码
cd post/db/rocksdb/reproduce && ./build_rocksdb.sh
export DB_BENCH=$PWD/rocksdb/_bin/db_bench && ./run_db_bench.sh

本机实测 (WSL2,i9-12900K,RocksDB 9.4.0,2026-07-07;以下输出经删减):

text 复制代码
fillrandom   :       2.448 micros/op 408456 ops/sec 0.490 seconds 200000 operations
Uptime(secs): 0.5 total
Cumulative compaction: 0.00 GB write ... Comp(cnt) 0
Write Stall (count): total-delays: 0, total-stops: 0

解读 :200k 写、约 0.5s 内后台 可能尚无 compactionComp(cnt)=0)------这是 benchmark 过短 的演示限制,不是 leveled 失效。观察层级应拉长 --num/--duration,或 --use_existing_db=1 再跑 stats

bash 复制代码
db_bench --benchmarks=fillrandom,stats --num=200000 --value_size=100 \
  --compaction_style=0 --statistics=1 --db=<path>

九、与相邻篇目分工

话题 本篇 他处
WA/RA/SA 公式与策略对比 引用 storage/31
toy 引擎 leveled 不变式(C 实现、Version/MANIFEST) 不展开 lsm-tree 第 4 篇 Compaction --- DIY 五篇教「归并迭代器 + 层级规则」
Flink 嵌入层(subtask 独立 DB、增量 checkpoint 与 L0 叠加) 不展开 stream/12 RocksDB State Backend
Universal / FIFO / write stall 状态机 仅 §4.1 决策链 第 11 篇
stall 生产排查(GetProperty / LOG) 不展开 第 17 篇
CompactionJob 并发与 RateLimiter 不展开 第 12 篇
CompactionIterator tombstone 不展开 第 8 篇

三条阅读线 :想 自己写 LSM → lsm-tree 系列;想 读 RocksDB 9.x 源码 → 本系列 10--12;想 Flink 状态慢 / checkpoint 变长 → stream/12 现象 + 本篇 score/stall 阈值 + 第 11 篇状态机。


十、小结

  • Leveled 不变式 :L0 可重叠 ;L1+ key 范围不重叠,点查每层至多一个 SST。
  • Size ratio TT T 与 max_bytes_for_level_base 决定各层容量与近似 WAdynamic level bytes 动态计算 base_level_,L0 目标层非固定 L1。
  • ComputeCompactionScore 驱动 compaction 触发;LevelCompactionPicker 负责 L0→Lbase、Li→Li+1、Intra-L0 与 trivial move 选型。
  • L0 文件数是 RAwrite stall 的共同杠杆;§4.1 给出 score→delay→stop 决策链;exp10 在短跑条件下可能 看不到 compaction/stall ,须延长 workload 再读 stats
  • Merge Bush / Dostoevsky 锚定 leveled(本篇)vs tiering(Universal,第 11 篇)的 WAWA WA/ RARA RA 定量争论;RocksDB picker 是工程实现,不是论文复现。

上一篇Block Cache 与 Bloom

下一篇Universal、FIFO 与 Write Stall

返回 系列目录


参考资料

源码(A 级)

  1. RocksDB 9.xdb/version_set.ccdb/compaction/compaction_picker_level.ccoptions/cf_options.h(stall 阈值 sanitize)。

论文(A 级)

  1. O'Neil et al., The Log-Structured Merge-Tree (LSM-Tree) , Acta Informatica 1996 --- LSM 概念起源。
  2. Dayan & Idreos, The Log-Structured Merge-Bush & the Wacky Continuum , SIGMOD 2017 --- tiering / Universal 理论端。
  3. Dayan & Idreos, Dostoevsky: Better Space-Time Trade-offs for LSM-Tree Based Key-Value Stores , SIGMOD 2018 --- leveled vs tiering 插值框架。

本站相关

  1. storage/31storage/32 --- 公式与调参 cookbook。
  2. lsm-tree 第 4 篇 Compaction --- toy leveled 实现。
  3. 第 6--9、11--12 篇stream/12

实验(A 级,本机)

  1. reproduce/output/exp10_leveled_stats.txt(RocksDB 9.4.0,WSL2,2026-07-07)--- 短跑 Comp(cnt)=0stall=0,见第八节解读。
相关推荐
ltl1 小时前
DuckDB 向量化与 Morsel-Driven Pipeline
数据库
NJCloud1 小时前
TiDB 集群部署与 MySQL 兼容性适配实战
linux·运维·数据库·mysql·tidb
喜欢打篮球的普通人2 小时前
LLVM Backend Lowering 从入门到实战:把 IR 变成机器码的完整链路
android·java·数据库
SomeOtherTime4 小时前
Postgresql触发器实现对表的日志审计
数据库·postgresql
这个DBA有点耶4 小时前
Oracle 迁移金仓兼容性评估指南:5 大维度深度拆解
数据库·oracle·架构
李可以量化5 小时前
Redis 从了解到精通(二)下:发布订阅进阶命令、量化场景落地与避坑指南
数据库·redis·python·qmt·ptrade
添砖java‘’5 小时前
Redis中常用数据结构
数据库·redis·缓存
用户3169353811836 小时前
MyBatis Mapper XML 文件解读
数据库