原文发布于 quant67.com,转载请保留出处。
第 6 篇 的 VersionSet::LogAndApply 把 flush 产出的 SST 挂进 LSM 快照;第 9 篇 说明 L0 文件多时 读放大 如何上升。Leveled Compaction 是 RocksDB 默认策略(CompactionStyle::kCompactionStyleLevel):后台 CompactionJob 在 LevelCompactionPicker 选定的输入集上归并,把数据逐层下推,同时尽量维持 L1 及以下 key 范围不重叠 的不变式。
本文是系列 第 10 篇 ,源码锚点:RocksDB 9.x 的 db/version_set.cc(ComputeCompactionScore、CalculateBaseBytes)、db/compaction/compaction_picker_level.cc、db/compaction/compaction_picker.h。写放大(WA)、读放大(RA)、空间放大(SA) 的定义与 Leveled 近似公式见 storage/31 LSM 工程调优------本篇只写 RocksDB 内核如何维护层级结构与触发 compaction,不重复该文推导表格。
版本锚定 :RocksDB 9.x (
facebook/rocksdbv9.10.0 及相邻 release)。实验:reproduce/run_db_bench.shexp10 (fillrandom,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::CalculateBaseBytes(db/version_set.cc)为每层填充 level_max_bytes_:
- L1 上限 =
max_bytes_for_level_base(默认 256 MiB 量级,可调)。 - Li( i≥2)上限 = Li−1×T×MaxBytesMultiplerAdditional(i−1),其中 T =
max_bytes_for_level_multiplier(默认 10)。
这与 storage/31 式 WAleveled≈T×(h−1) 的假设一致:每层向下 compact 时,约需与 整层 L(i+1) 中 key 范围重叠的 ~ T 个文件做归并,用户字节在磁盘上被重写约 T 次/层。
动态层级字节(level_compaction_dynamic_level_bytes = true)
大 CF 常用 dynamic level bytes :根据 实际数据分布 计算 base_level_ (L0 直接 compact 的目标层),使最底层承载绝大部分数据,上层可保持较空。CalculateBaseBytes 会:
- 找到 第一个非空层 与 最大层 的实际大小;
- 令
base_level_满足「Lbase 目标大小 ≈max_bytes_for_level_base」; - 向上/向下推导各层
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 compaction (PickIntraL0Compaction / PickSizeBasedIntraL0Compaction):仅在 L0 内部归并,降低 sorted run 数,缓解 write stall ,且与 L0→Lbase 可并行(注释:Intra-L0 compaction is independent...)。
L0 trivial move (TryPickL0TrivialMove):若单个 L0 文件 key 范围 完全落在 Lbase 某文件间隙中、归并无实质 key 重叠,可直接 改层 而不重写数据------这是 WA 上的重要捷径。
四、Compaction Score 与触发条件
VersionStorageInfo::ComputeCompactionScore(db/version_set.cc)为每层计算 score ;score ≥ 1 表示该层需要 compaction(与历史「字节/目标比」语义兼容,9.x 对大 score 还有 10× 缩放 用于优先级,阈值仍为 1.0)。
L0 score
对 Leveled / Universal / FIFO,L0 基础 score 为:
score0=level0_file_num_compaction_triggernum_sorted_runs
Leveled 额外考虑 L0 总 compensated 大小 与 Lbase 上限关系,避免 L0 单文件过大导致后续 巨型 L0→Lbase(见源码注释:平衡 L0 文件数与字节)。
4.1 从 score 到 write stall 的决策直觉
Compaction score 与 write stall 是同一债务链上的两环:score 驱动后台 CompactionJob 调度;当前台 flush 快于 compaction 消化时,L0 文件数 (Leveled 下即 score0 的分子)先触达 stall 阈值,不等 score 逻辑走完才停写。
SanitizeOptions 强制单调关系(A 级源码 options/cf_options.cc):
level0_file_num_compaction_trigger≤level0_slowdown_writes_trigger≤level0_stop_writes_trigger
| 阶段 | 典型条件(Leveled) | 内核动作 | 读者该看什么 |
|---|---|---|---|
| 正常 | L0 文件数 < slowdown | 无 stall | rocksdb.stats 各层文件数 |
| Compaction 欠债 | L0 score ≥1 | 调度 L0→Lbase / Intra-L0 | compaction-pending、num-running-compactions |
| Delay | L0 数 ≥ level0_slowdown_writes_trigger |
WriteController 限速 sleep |
LOG Stalling writes;actual-delayed-write-rate |
| Stop | L0 数 ≥ level0_stop_writes_trigger |
写线程 bg_cv_.Wait() |
is-write-stopped=true;STALL_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=MaxBytesForLevel(i)∑file_compensated_size(未在 compact 中)
LevelCompactionPicker::NeedsCompaction 扫描各层 score ≥ 1,或存在 TTL / periodic / bottommost 标记文件等,即返回 true。
五、LevelCompactionPicker 选 compaction 流程
LevelCompactionPicker(db/compaction/compaction_picker_level.cc)通过内部 LevelCompactionBuilder 分步构造 Compaction 对象:
要点:
SetupInitialFiles:从 最高 score 层 选起始文件;L0 输出到base_level(),Li( i>0)输出到 i+1。ExpandInputsToCleanCut:扩展输入边界,保证 无遗漏 overlap 文件 ,且 L1+ 输入在 key 范围上形成 clean cut。- 并发约束 :L0 同时只允许 有限 L0→Lbase job(
level0_compactions_in_progress()非空时PickFileToCompact跳过新的 L0→Lbase)。 - CompactionReason :L0 文件数触发为
kLevelL0FilesNum;L1+ 字节超限为kLevelMaxLevelSize(见SetupInitialFiles内赋值)。
Compaction 优先级与 round-robin
CompactionOptions 中 compaction_pri (如 kMinOverlappingRatio、kRoundRobin)影响 同层多文件 时先 pick 哪一个。Round-robin 可一次 pick 连续多个 L0 文件 组成大 compaction(SetupOtherFilesWithRoundRobinExpansion),提高单次 job 吞掉 L0 debt 的效率,但单次 job CPU / 磁盘峰值更高------与第 12 篇 max_subcompactions 配合调优。
Picker 接口在 db/compaction/compaction_picker.h;CF 构造时按 compaction_style 实例化(ColumnFamilyData 内 LevelCompactionPicker / UniversalCompactionPicker / FIFOCompactionPicker)。
六、文献中的 Leveled 模型与 Tiering 争论
RocksDB 的 LevelCompactionPicker 实现的是 工业 leveled compaction (源自 LevelDB),不是 1996 年 LSM 论文里的唯一形态。读源码时应与下列 A 级论文 对照------它们定义 size ratio T、sorted run 数与 WA/ RA 的定量争论。
6.1 代表 work 对照
| 工作 | venue | 优化目标 | 与 RocksDB 默认 leveled 的关系 |
|---|---|---|---|
| O'Neil et al., LSM-Tree | Acta Informatica 1996 | 顺序写 + 多级归并 | 概念起源 |
| Dayan & Idreos, Merge Bush | SIGMOD 2017 | Tiering:同层多 run,降低 WA | 对应 Universal 的理论端(第 11 篇) |
| Dayan & Idreos, Dostoevsky | SIGMOD 2018 | 在 leveled/tiering 间插值 | 解释 T 与层数 是旋钮;非 magic number |
| LevelDB / RocksDB leveled | 工程 | L0 重叠、L1+ 不重叠 | 本篇拆解对象 |
Dostoevsky 的核心结论(简述,细节见原文):在 固定写放大预算 下,纯 leveled 与纯 tiering 都不是全局最优;RocksDB 的 dynamic level bytes 与 Universal 是工程侧对同一 trade-off 的响应,不是论文算法的逐行移植。
6.2 假设---结论链
| 假设 | 若成立 | 若失效(生产常见) |
|---|---|---|
| L0 文件数低于 trigger | RA 低、少 stall | 写突发 → 第 11 篇 |
| T=10 且 key 均匀 | WA≈T×(h−1)(storage/31) | 倾斜 key → 巨型 merge |
| Compaction 跟得上 flush | stats 见稳态层级 |
短 benchmark 可能 0 次 compact(见第八节实测) |
6.3 开放问题
- Workload-aware picker :learned / adaptive compaction 在研究与 RocksDB 实验特性中均有线索,无统一 GA 默认。
- Remote compaction :存储分离后 leveled 的 网络读放大 是否抵消 WA 收益------须分论文与产品 release 引用。
- Intra-L0 最优性:何时优于 L0→Lbase 仍靠 trace/stats,缺 closed-form 规则。
七、与三种放大的衔接(不重复 storage/31 全文)
| 机制 | 主要影响的放大 | 内核落点 |
|---|---|---|
| L0 文件数 / Intra-L0 | RA(L0 扫描次数) | ComputeCompactionScore、Intra-L0 picker |
| Size ratio T、层数 h | WA | MaxBytesForLevel、max_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 内后台 可能尚无 compaction (Comp(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 T 与
max_bytes_for_level_base决定各层容量与近似 WA ;dynamic level bytes 动态计算base_level_,L0 目标层非固定 L1。 ComputeCompactionScore驱动 compaction 触发;LevelCompactionPicker负责 L0→Lbase、Li→Li+1、Intra-L0 与 trivial move 选型。- L0 文件数是 RA 与 write stall 的共同杠杆;§4.1 给出 score→delay→stop 决策链;exp10 在短跑条件下可能 看不到 compaction/stall ,须延长 workload 再读
stats。 - Merge Bush / Dostoevsky 锚定 leveled(本篇)vs tiering(Universal,第 11 篇)的 WA/ RA 定量争论;RocksDB picker 是工程实现,不是论文复现。
上一篇 :Block Cache 与 Bloom
下一篇 :Universal、FIFO 与 Write Stall
返回 系列目录
参考资料
源码(A 级)
- RocksDB 9.x :
db/version_set.cc;db/compaction/compaction_picker_level.cc;options/cf_options.h(stall 阈值 sanitize)。
论文(A 级)
- O'Neil et al., The Log-Structured Merge-Tree (LSM-Tree) , Acta Informatica 1996 --- LSM 概念起源。
- Dayan & Idreos, The Log-Structured Merge-Bush & the Wacky Continuum , SIGMOD 2017 --- tiering / Universal 理论端。
- Dayan & Idreos, Dostoevsky: Better Space-Time Trade-offs for LSM-Tree Based Key-Value Stores , SIGMOD 2018 --- leveled vs tiering 插值框架。
本站相关
- storage/31、storage/32 --- 公式与调参 cookbook。
- lsm-tree 第 4 篇 Compaction --- toy leveled 实现。
- 第 6--9、11--12 篇;stream/12。
实验(A 级,本机)
reproduce/output/exp10_leveled_stats.txt(RocksDB 9.4.0,WSL2,2026-07-07)--- 短跑 Comp(cnt)=0 、stall=0,见第八节解读。