第
10 篇 讲 Leveled 如何通过层级 score 与
LevelCompactionPicker 控制 L0 与 Size
Ratio。生产上还有
Universal(写放大更友好)、FIFO(队列
/
时序数据)、TTL(过期淘汰)等策略;无论哪种
style,当前台写入快于 flush + compaction 时,都会进入同一套
Write Stall
机制:WriteController 对全 DB
Stop 或
Delay,DBImpl::DelayWrite
阻塞或降速 WriteImpl。
本文是系列 第 11 篇,源码锚点:RocksDB
9.x 的
db/column_family.cc(GetWriteStallConditionAndCause、RecalculateWriteStallConditions)、db/write_controller.h、db/db_impl/db_impl_write.cc(DelayWrite)、db/compaction/compaction_picker_universal.cc、db/compaction/compaction_picker_fifo.cc。通用
write stall 背景见 storage/67
写入性能优化 第七节——本篇写 内核状态机与
LOG/Property 入口。
版本锚定:RocksDB 9.x。
level0_slowdown_writes_trigger/level0_stop_writes_trigger默认值随版本可能调整,以options/cf_options.h与SanitizeOptions为准。实验:reproduce/run_db_bench.shexp11。
一、三种 Compaction Style 在内核中的位置
ColumnFamilyData 构造 compaction picker
时分支(db/column_family.cc):
compaction_style |
Picker 类 | 典型场景 |
|---|---|---|
kCompactionStyleLevel |
LevelCompactionPicker |
默认;点查多、可接受较高 WA |
kCompactionStyleUniversal |
UniversalCompactionPicker |
写密集;减少层间反复合并 |
kCompactionStyleFIFO |
FIFOCompactionPicker |
仅追加、按时间淘汰;常配合 TTL |
TTL 不是独立 style,而是 CF 选项
ttl(秒):ComputeCompactionScore
与 picker 会把 超过 TTL 的 SST 纳入
ExpiredTtlFiles(),触发 compaction 或(FIFO
下)删除。Universal 下 SanitizeOptions 会把
periodic_compaction_seconds 与 ttl
对齐处理(见 column_family.cc 注释)。
Universal 与 Leveled 的分工差异
- Score:Universal 在
ComputeCompactionScore中把 L1+ 非空层 也计入 L0 的 sorted run 数,whole-DB 更像「多层 run 归并」而非严格 L(i)→L(i+1)。 - Write stall 阈值:仍共用
level0_slowdown_writes_trigger/level0_stop_writes_trigger(对 Universal 而言语义是 run 数,不仅是物理 L0 文件数)。 - 读放大:Universal 通常 run 数少、单层文件更大,RA 与 SA 的折中与 Leveled 不同(公式级讨论见 storage/31,本篇不展开)。
FIFO 与 TTL
FIFO 按 文件时间顺序 淘汰:当总大小超过
compaction_options_fifo.max_table_files_size
时删除最老文件;若 allow_compaction 为
true,score 也会参考 L0 文件数。SanitizeOptions
要求 FIFO + TTL 时常设
max_open_files = -1,否则 open
文件数限制与按序删除冲突。
二、Write Stall 触发条件(CF 级)
ColumnFamilyData::GetWriteStallConditionAndCause(db/column_family.cc)在
持有 DB mutex 的路径上,根据当前
SuperVersion 统计返回
(WriteStallCondition, WriteStallCause):
stateDiagram-v2
direction TB
[*] --> Normal
Normal --> Delayed: L0 slowdown / soft pending / memtable-1
Normal --> Stopped: L0 stop / hard pending / memtable full
Delayed --> Stopped: 条件恶化
Stopped --> Delayed: compaction 消化 debt
Delayed --> Normal: debt 下降
Stopped --> Normal: 恢复
Stopped(WriteStallCondition::kStopped)
— 对应 WriteController::GetStopToken():
| 条件 | Cause 枚举 | 典型阈值选项 |
|---|---|---|
未 flush MemTable 数 ≥
max_write_buffer_number |
kMemtableLimit |
默认 2–4 |
L0 文件数(或 Universal run 数)≥
level0_stop_writes_trigger |
kL0FileCountLimit |
默认 36(可调) |
estimated_compaction_needed_bytes ≥
hard_pending_compaction_bytes_limit |
kPendingCompactionBytes |
默认 256 GiB 量级 |
Delayed(WriteStallCondition::kDelayed)
— 对应 GetDelayToken(rate):
| 条件 | Cause |
|---|---|
MemTable 数 ≥
max_write_buffer_number - 1(且 >3
等前提) |
kMemtableLimit |
L0 数 ≥ level0_slowdown_writes_trigger |
kL0FileCountLimit |
pending bytes ≥
soft_pending_compaction_bytes_limit |
kPendingCompactionBytes |
SanitizeOptions 强制
level0_stop_writes_trigger ≥
level0_slowdown_writes_trigger ≥
level0_file_num_compaction_trigger,避免阈值倒挂。
2.1 Write stall 决策直觉(Leveled vs Universal)
把 stall 想成 三级背压,与 第 10 篇 §4.1 的 score 链衔接:
flowchart LR
W["Write / Flush"] --> L0["L0 sorted runs ↑"]
L0 --> T{"runs ≥ compaction_trigger?"}
T -->|yes| CP["Schedule compaction"]
L0 --> D{"runs ≥ slowdown?"}
D -->|yes| DEL["DelayWrite + GetDelay"]
L0 --> S{"runs ≥ stop?"}
S -->|yes| STP["StopWrite bg_cv_.Wait"]
CP --> L0
DEL --> W
STP --> W
| 旋钮 | 作用 | 写路径表现 |
|---|---|---|
level0_file_num_compaction_trigger |
score \(\ge 1\),应调度 compaction | 仍全速写;后台 job 增多 |
level0_slowdown_writes_trigger |
Delay token | 每次 Put 可能
sleep;STALL_MICROS 上升 |
level0_stop_writes_trigger |
Stop token | 写线程阻塞至 flush/compaction 安装新 SuperVersion |
soft/hard_pending_compaction_bytes_limit |
pending 字节过大 | 同 Delay/Stop,cause 为
kPendingCompactionBytes |
max_write_buffer_number |
immutable memtable 堆积 | MemTable cause;与 flush 线程数相关(第 5 篇) |
Leveled vs Universal 差异(Dayan & Idreos, Merge Bush SIGMOD 2017 → Universal picker):Leveled 的 stall 计数主要看 物理 L0 文件数;Universal 把 L1+ 非空层 也计入 sorted run(§一)——相同阈值数字下 Universal 更易进入 Delay/Stop,但单次 compaction 吞 run 的方式不同,不能用同一套 trigger 直接横比策略优劣。
策略争论锚点:Merge Bush 主张 tiering 降 \(WA\);Dostoevsky(SIGMOD 2018)说明固定预算下 leveled/tiering 皆非全局最优——RocksDB 默认 Leveled + 可选 Universal 是工程折中,不是论文最优解的移植。选型 POC 见 第 18 篇。
延迟数字:本篇 不给出
stall 毫秒级 benchmark;本机 exp11 在 WSL2 快盘上
stall.micros=0(§七)。线上 tail 须结合
LOG、rocksdb.is-write-stopped
与磁盘带宽自测。
三、WriteController:DB 级 Stop 与 Delay
多个 Column Family 共享 一个
WriteController(挂在
ColumnFamilySet)。Token
语义(db/write_controller.h):
StopWriteToken:total_stopped_++;存在任一 stop token 时IsStopped()为真,所有 CF 写路径等待。DelayWriteToken:total_delayed_++;NeedsDelay()为真时,每次写调用GetDelay(clock, num_bytes)返回应 sleep 的微秒数。CompactionPressureToken:compaction 落后时NeedSpeedupCompaction()为真,可提高后台 compaction 并发上限(与第 12 篇线程池联动)。
delayed_write_rate 默认由
MutableDBOptions::delayed_write_rate
注入(常见默认 64 MiB/s
量级);SetupDelay(column_family.cc
匿名命名空间)在 已 delay 时根据
estimated_compaction_needed_bytes
增减速率(kIncSlowdownRatio /
kDecSlowdownRatio),接近 stop 时乘以
kNearStopSlowdownRatio
进一步降速。
四、RecalculateWriteStallConditions 与 LOG
每次安装新 SuperVersion(flush /
compaction 完成、LogAndApply
后),ColumnFamilyData::InstallSuperVersion
调用
RecalculateWriteStallConditions:
- 调用
GetWriteStallConditionAndCause; - 释放旧 token,按新状态申请 Stop 或 Delay token;
- 写
ROCKS_LOG_WARN并累加InternalStatsCF 计数器。
L0 stop 典型 LOG(源码原文模式):
[<cf>] Stopping writes because we have <N> level-0 files
L0 delay 典型 LOG:
[<cf>] Stalling writes because we have <N> level-0 files rate <bytes/sec>
MemTable stop:
Stopping writes because we have <N> immutable memtables ... max_write_buffer_number is set to <M>
若 stall 发生但 已有 L0 compaction
在进行,stats 会额外记
L0_FILE_COUNT_LIMIT_*_WITH_ONGOING_COMPACTION,用于区分「compaction
已努力但仍跟不上」与「compaction 未调度」。
五、DBImpl::DelayWrite:写路径上的状态机
Leader write thread 在 WriteImpl 中若检测到
write_controller_.IsStopped()
或 NeedsDelay(),进入
DBImpl::DelayWrite(db/db_impl/db_impl_write.cc):
- Delay 阶段:主队列调用
GetDelay;若WriteOptions::no_slowdown,返回Status::Incomplete("Write stall")而不睡眠。 - 否则
BeginWriteStall,释放 DB mutex,按delay微秒 sleep(循环中每 ~1ms 检查是否仍NeedsDelay)。 - Stop 阶段:若
IsStopped(),在bg_cv_上 Wait(mutex 释放),直到 compaction/flush 安装新 SuperVersion 并SignalAll。 - 累计
STALL_MICROS、WRITE_STALLhistogram、kIntStatsWriteStallMicros。
sequenceDiagram
participant WT as WriteThread leader
participant WC as WriteController
participant BG as Flush/Compaction
WT->>WC: IsStopped / NeedsDelay?
alt Delay
WT->>WC: GetDelay(num_bytes)
WT->>WT: Sleep(delay_us)
end
alt Stop
WT->>WT: bg_cv_.Wait()
BG->>WT: InstallSuperVersion SignalAll
end
WT->>WT: WriteImpl continues
这与 storage/67
写停顿流程图 一致,但 sleep + condition
variable 的细节以
db_impl_write.cc::DelayWrite
为准。
5.1 文献与工业对照:stall 是反馈控制,不是「bug」
Write stall 在文献与工业界的定位是 背压(backpressure),不是实现失误:
| 来源 | 类型 | 观点 |
|---|---|---|
| RocksDB Wiki, Write Stalls | A 级文档 | L0 / memtable / pending bytes 超阈值 → 故意 降速或停写,给 compaction 时间 |
| Sears & Ramanan, bLSM (SIGMOD 2012) | A 级论文 | Many-core compaction + 控制 L0 run 数,减少 stall 持续时间;RocksDB 多线程 compaction 属同思路的工程演化 |
| Mark Callaghan, RocksDB Blog Write Stalls 系列 | B 级 | 生产 trace 中 stall 与 I/O 饱和、小 memtable 的关联(作现象线索,不单独支撑数值) |
| Flink 运维文档 | B 级 | state 膨胀时 checkpoint 变长 常与 RocksDB L0/compaction 叠加(stream/13) |
争论点:更激进的 write
throttle(早 delay、晚 stop)能否在
不牺牲读 SLA 下降低
tail?论文侧无统一答案;RocksDB 用
level0_slowdown_writes_trigger 与
SetupDelay 的 自适应降速
是工程折中,不是最优控制理论解。
5.2 工程间隙
| 论文/教程 | 生产(Flink/TiKV) |
|---|---|
| 单 CF、均匀写 | 多 CF、KeyGroup 倾斜 |
stall 仅影响 Put 延迟 |
叠加 checkpoint 读 SST、JVM GC |
| 长时间 steady-state | 突发 barrier、Region 迁移 |
六、GetProperty 与 db_bench Statistics
生产判读常用接口(详见 storage/67 7.3 节):
db->GetProperty("rocksdb.is-write-stopped", &value);
db->GetProperty("rocksdb.actual-delayed-write-rate", &value);
db->GetProperty("rocksdb.num-immutable-mem-table", &value);
db->GetProperty("rocksdb.compaction-pending", &value);
db->GetProperty("rocksdb.level0-slowdown-writes-trigger", &value);
db->GetProperty("rocksdb.level0-stop-writes-trigger", &value);打开 Statistics 时关注
tick(include/rocksdb/statistics.h):
| Tick / 计数 | 含义 |
|---|---|
STALL_MICROS |
写路径 stall 总微秒 |
NUMBER_BLOCKING_FLUSHES / MEMTABLE
相关 |
flush 背压 |
CF stats:L0_FILE_COUNT_LIMIT_DELAYS /
STOPS |
L0 触发的 delay/stop 次数 |
db_bench --statistics=1 结束时会打印
Stalls(count) 行(见 storage/32
示例格式),例如
level0_slowdown、level0_numfiles
等分类计数——具体数字须本机运行得到。
七、实验 exp11:刻意触发 L0 stall
reproduce/run_db_bench.sh
exp11 用 极低 L0 阈值 在
fillrandom 中 试图 放大 stall:
db_bench --benchmarks=fillrandom --num=500000 --value_size=256 \
--level0_slowdown_writes_trigger=2 \
--level0_stop_writes_trigger=4 \
--statistics=1 --db=<path>本机实测(WSL2,i9-12900K,RocksDB 9.4.0,2026-07-07;以下经删减):
fillrandom : 2.901 micros/op 344660 ops/sec 1.451 seconds 500000 operations
rocksdb.stall.micros COUNT : 0
rocksdb.db.write.stall ... COUNT : 0
解读:尽管
slowdown=2、stop=4,在
高速 SSD + 1.45s 跑完 500k 写
的条件下,可能仍观察不到 stall——L0
文件数未持续超过阈值,或 compaction 足够快。这
不否定 第五节状态机;只说明 exp11
是机制验证脚本,不是保证触发 stall 的
benchmark。要稳定看到 stall,需:更长
--duration、更慢磁盘、或
人为 pause compaction(仅实验环境)。
注意:阈值过低的配置 仅用于实验;生产见 storage/32 与 第 17 篇。
7.1 开放问题
- 跨 CF 公平 stall:多 CF 共享
WriteController时,单 CF L0 爆炸是否过度拖累全局?社区讨论见 RocksDB GitHub issues;无单一论文结论。 - Predictive throttle:能否用
estimated_compaction_needed_bytes训练提前降速,避免 hard stop?研究线与 AutoTune 实验相关,生产默认仍以阈值为主。 - Universal vs Leveled stall 语义:Universal 把 L1+ run 计入 L0 score——同样阈值下 stall 频率不同;跨策略比较须固定 workload(第 10 篇 Dostoevsky 框架)。
八、与相邻篇目分工
| 话题 | 本篇 | 他处 |
|---|---|---|
| Leveled score / Intra-L0 / §4.1 score→stall 链 | 不展开 | 第 10 篇 |
| toy 引擎 compaction 策略与最小堆归并 | 不展开 | lsm-tree 第 4 篇 — 无 WriteController,但 L0 文件数→读退化同源 |
| Flink TM 上 stall 表现为 checkpoint/sync 变长 | 现象 + 阈值 | stream/12 嵌入路径;stream/13 调优 |
| RateLimiter 与 compaction 并发 | 不展开 | 第 12 篇 |
| Universal 放大公式 | 引用 | storage/31 |
| 生产 LOG / GetProperty 排查 | 不展开 | 第 17 篇 |
分工一句话:lsm-tree 教 归并结构与策略;rocksdb 10–12 教 RocksDB 9.x 状态机;stream/12 教 Flink 如何把 stall 叠在 barrier/checkpoint 上。
九、小结
- Universal / FIFO / TTL 改变
picker 与 score,但 write
stall 仍由
GetWriteStallConditionAndCause统一判定,经WriteControllertoken 作用于全 DB。 level0_slowdown_writes_trigger→ Delay(可变速率);level0_stop_writes_trigger→ Stop(bg_cv_硬等)。RecalculateWriteStallConditions连接 compaction 进度与 LOG / InternalStats;DelayWrite是写线程侧的 sleep + wait 实现。- GetProperty 与 Statistics 是线上判读 stall 的一手入口;exp11 在快盘上 可能 stall=0,须延长负载或换磁盘再验证。
- Stall 是 背压机制(Wiki A 级);bLSM(SIGMOD 2012)代表 多核 compaction 减 stall 的学术线,RocksDB 实现以源码为准。
下一篇:并发 Compaction 与 Rate Limiter
返回 系列目录
参考资料
源码与 Wiki(A 级)
- RocksDB
9.x:
db/column_family.cc;db/write_controller.h;db/db_impl/db_impl_write.cc。 - RocksDB Wiki, Write Stalls / Universal Compaction / FIFO Compaction。
论文(A 级)
- Sears & Ramanan, bLSM: A General Purpose Log Structured Merge Tree, SIGMOD 2012 — 多核 compaction 与 stall 持续时间。
- Dayan & Idreos, The Log-Structured Merge-Bush, SIGMOD 2017;Dostoevsky, SIGMOD 2018 — Universal vs Leveled 定量框架。
本站相关
实验(A 级,本机)
- exp11(§七):WSL2 + RocksDB
9.4.0,
stall.micros=0— 不作为「stall 很轻」的泛化结论,仅证明短跑/快盘可能未触阈。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【RocksDB 内核机制】LevelDB · WAL · Compaction · Column Family · 生产嵌入
补全存储引擎三角最后一角:从 LevelDB 基线与 RocksDB 架构演进,到 WAL/MemTable/SST 写路径、Get/Iterator 读路径、Leveled/Universal compaction 与 write stall,再到 Column Family、事务、Checkpoint 与 Flink/TiKV 嵌入对照。
【RocksDB 内核机制】经典故障与排查
用 GetProperty 与 LOG 钉住 L0 堆积、compaction 落后、Block Cache 未命中与 ENOSPC write stall 四类生产故障;交叉引用 storage/76 磁盘耗尽链,给出 db_bench 复现步骤而不伪造 stats 数字。
【RocksDB 内核机制】Leveled Compaction:层级不变式与 CompactionPicker
从 L0 重叠与 L1+ 不重叠不变式、Size Ratio 与 base_level 动态计算,到 LevelCompactionPicker 如何选 L0→Lbase 与 Intra-L0;三种放大与 storage/31 衔接,exp10 用 db_bench stats 观察层级分布。
【WiredTiger 内核】与 PG / InnoDB / RocksDB 机制对照
按缓冲池、写前日志、MVCC 落点、脏页写出与故障模式对照 PostgreSQL、InnoDB、RocksDB 与 WiredTiger;只谈机制与代价,不写未实测延迟排名。