第
3 篇 把 RocksDB 相对 LevelDB 的架构 diff
地图画完:多线程 flush/compaction、Column
Family、RateLimiter。写路径的第一条硬约束还没落地:用户调用
DB::Put / Write
时,数据如何在崩溃后可恢复的前提下进入
MemTable?
lsm-tree DIY
第 2 篇 从零实现了 WAL record 与跳表 MemTable;RocksDB
沿用 LevelDB 的 WAL 分块格式,但在
DBImpl::WriteImpl 里叠加了 WriteThread
Group Commit、sync /
disableWAL、recyclable log 与
MANIFEST 中 WAL 元数据同步。本文是系列
第 4 篇,只钉写路径入口,不展开 flush(第 5
篇)与 SST(第 6
篇)。
版本锚定:RocksDB 9.x(源码路径以
facebook/rocksdbv9.9.x 为准;本机实验脚本默认 checkout v9.10.0,若 tag 不可用可改ROCKSDB_TAG)。环境:WSL2 Linux 6.6.x;实验见reproduce/run_db_bench.shexp04。
一、写路径全景:WriteBatch 是唯一载体
RocksDB 公开 API 的 Put /
Delete / Merge 最终都组装进
WriteBatch,再经
DBImpl::Write 提交:
flowchart LR
API["DB::Put / Delete / Write"] --> WB["WriteBatch rep_"]
WB --> WI["DBImpl::WriteImpl"]
WI --> GT["WriteThread::JoinBatchGroup"]
GT --> WAL["WriteToWAL"]
WAL --> MT["WriteBatchInternal::InsertInto → MemTable"]
MT --> SEQ["分配 sequence number"]
| 阶段 | 源码锚点 | 持久化? |
|---|---|---|
| 组批 | db/write_thread.cc
EnterAsBatchGroupLeader |
否 |
| WAL 追加 | db/log_writer.cc
AddRecord |
是(顺序写 .log) |
| sync | db/db_impl/db_impl_write.cc
MarkLogsSynced |
是(fsync 边界) |
| MemTable | db/write_batch.cc handler |
否(内存;崩溃靠 WAL 重放) |
| 序列号 | db/version_set.*
LastSequence |
逻辑顺序(WAL 与 MemTable 共享) |
关键结论:对默认配置(WAL
开启、disableWAL=false),MemTable
写入发生在 WAL
追加成功之后;WriteOptions::sync=true
时,在 MemTable 写入前还要完成 log 文件 +
目录 的 sync,并把已 sync 的 WAL 元数据写入
MANIFEST(ApplyWALToManifest)。
二、WAL record 格式:32KB Block 与分片
WAL 物理布局在 db/log_writer.h 文件头注释与
db/log_format.h 常量中定义。
2.1 Block 对齐
+-----+-------------+--+----+----------+------+-- ... ----+
| r0 | r1 |P | r2 | r3 | r4 | |
+-----+-------------+--+----+----------+------+-- ... ----+
<--- kBlockSize ------>|<-- kBlockSize ------>|
kBlockSize = 32768(32 KiB):db/log_format.h。- 当前 Block 剩余空间放不下下一条 record 时,用
kZeroType填充 至 Block 边界,下一条 record 从新 Block 起始写。 - 这与 lsm-tree WAL 篇 的 DIY 实现一致,目的是让读端按固定 Block 边界做 recovery scan。
2.2 Legacy record header
每条 record 的 header(kHeaderSize = 7
字节):
| 字段 | 长度 | 含义 |
|---|---|---|
| CRC | 4 B | 覆盖 Type + Payload 的 CRC32C |
| Size | 2 B | Payload 长度 |
| Type | 1 B | 分片类型 |
Type 枚举(db/log_format.h
RecordType):
| 常量 | 值 | 用途 |
|---|---|---|
kFullType |
1 | 单 Block 内完整 record |
kFirstType / kMiddleType /
kLastType |
2–4 | 跨 Block 大 record 分片 |
kRecyclableFullType 等 |
5–8 | recycle_log_files 时在 header 加
log
number(kRecyclableHeaderSize = 11) |
kSetCompressionType |
9 | WAL 压缩类型元数据 |
kUserDefinedTimestampSizeType |
10–11 | UDT 列族 timestamp 宽度 |
log::Writer::AddRecord(db/log_writer.cc)负责:按剩余
Block 空间切分 Payload、选择 FULL/FIRST/MIDDLE/LAST、写入
header 与 CRC。空 Payload 也会 emit 一条 zero-length
record(便于边界对齐)。
2.3 WAL 里装什么
一次 WriteToWAL 通常写入
序列化后的整份 WriteBatch(多 CF
时可能有多条 record,并可能先写 UDT size record)。Recovery
时 log::Reader 逐 record 重放,再调用
WriteBatch handler 重建 MemTable——与 DIY 引擎「WAL 存 KV
字节流」不同,RocksDB WAL 存的是 带 CF
与操作类型的批格式,便于单条 WAL record
原子对应一个 WriteBatch。
三、WriteBatch 二进制布局与原子语义
include/rocksdb/write_batch.h
说明:同一 WriteBatch
内更新按添加顺序应用;batch.Put("k","v1"); batch.Delete("k"); batch.Put("k","v3")
最终 k 的值为 v3。
db/write_batch.cc 文件头给出
rep_ 布局(节选):
WriteBatch::rep_ :=
sequence: fixed64 // 提交前由 DB 填入
count: fixed32
data: record[count]
record :=
kTypeValue varstring varstring
kTypeDeletion varstring
kTypeColumnFamilyValue varint32 varstring varstring
... // Merge、DeleteRange、事务 XID 等
varstring := len: varint32 ; data: uint8[len]
| 设计点 | 含义 |
|---|---|
| 单 rep_ 缓冲区 | 多操作一次 Write() 提交;leader 把 group
内多个 batch 拼接 进同一 WAL record 或顺序
record |
| sequence 字段 | 由 WriteImpl 在写 WAL 前统一分配;每条
Put/Delete 占一个 sequence(seq_per_batch
模式例外) |
| CF 前缀 | kTypeColumnFamily* 带
column_family_id,为多 CF 共享 WAL 铺垫(第 13
篇) |
| 事务 record | kTypeBeginPrepareXID /
kTypeCommitXID 等;TransactionDB
路径,本文不展开 |
原子性边界:一个 WriteBatch
在 WriteImpl 单次 leader 周期
内要么整批进 WAL + MemTable,要么在 WAL
失败时整批不生效(已写 WAL 但未 sync
的边界见下一节)。RocksDB 不保证跨两次
Write() 调用的原子性——那是应用层或
TransactionDB 的职责。
四、DBImpl::WriteImpl:Group Commit 与 leader 路径
4.1 入口与校验
DBImpl::Write(db/db_impl/db_impl_write.cc)在
protection_bytes_per_key 等校验后调用
WriteImpl。
WriteImpl
先处理特殊模式(two_write_queues_ +
disable_memtable 的 WAL-only
Prepare、unordered_write、enable_pipelined_write),默认路径为:
- 构造
WriteThread::Writer w; write_thread_.JoinBatchGroup(&w)— 链入等待队列或成为 leader;- 若
w.state == STATE_GROUP_LEADER,执行 WAL + MemTable; - 否则等待 leader 完成后返回
w.FinalStatus()。
4.2 Group
Commit:EnterAsBatchGroupLeader
Leader 在
write_thread_.EnterAsBatchGroupLeader(&w, &write_group)(db/write_thread.cc)中从
newest_writer_ 链表
向后扫描,把 WriteOptions
兼容 的 writer 纳入同一
WriteGroup:
- 组大小受
max_write_batch_group_size_bytes限制; - 若 leader 的 batch 很小(\(\le
\max/8\)),上限收紧为
size + max/8,避免小写被大组拖慢; - 不兼容的 writer 暂挂到
r_list,leader 结束后再挂回主链。
统计项
WRITE_DONE_BY_OTHER(statistics.h)计数被
leader 代写的线程数——观察 Group Commit
是否生效的实用指标。
4.3 提交顺序(默认单 write queue)
Leader 在释放 db mutex 后(注释:WAL 与
memtable 由 leader 串行保护)大致顺序:
| 步骤 | 函数 / 动作 | 说明 |
|---|---|---|
| 1 | PreprocessWrite |
stall 检测、切换 WAL 文件等 |
| 2 | WriteToWAL(write_group, ...) |
拼接 batch 写入 log::Writer |
| 3 | 若 log_context.need_log_sync |
MarkLogsSynced +
ApplyWALToManifest |
| 4 | WriteBatchInternal::InsertInto |
写入各 CF 的 active MemTable |
| 5 | 更新 LastSequence |
与 WAL 中 sequence 一致 |
| 6 | ExitAsBatchGroupLeader |
唤醒 follower,置 STATE_COMPLETED |
WAL 与 MemTable 的顺序保证:崩溃
recovery 时先重放 WAL,尚未 flush 的数据不会丢;已 sync 的
WAL 在 MANIFEST 中有 kWalAddition /
synced 边界
记录(db/version_edit.h)。
五、sync、disableWAL 与持久性语义
5.1 LogContext
DBImpl::LogContext(db/db_impl/db_impl.h):
struct LogContext {
explicit LogContext(bool need_sync = false)
: need_log_sync(need_sync), need_log_dir_sync(need_sync) {}
bool need_log_sync = false;
bool need_log_dir_sync = false;
log::Writer* writer = nullptr;
LogFileNumberSize* log_file_number_size = nullptr;
};WriteImpl 构造
LogContext log_context(write_options.sync):仅当
sync=true 时 在 WAL 写入后触发文件
sync 与目录 sync。
WriteOptions |
WAL | 崩溃丢数据? | 典型场景 |
|---|---|---|---|
| 默认 | 写,不 sync | 可能丢最近未 sync 批次 | 吞吐优先 |
sync=true |
写 + fsync | 已返回 OK 的写不丢(单进程语义) | 金融类关键写 |
disableWAL=true |
跳过 | MemTable 未 flush 前全丢 | 可重建缓存 |
disableWAL=true +
sync=true |
非法 | — | WriteImpl 返回
InvalidArgument |
manual_wal_flush_ 与
WriteOptions::disableWAL 组合可延迟 WAL
flush,由 FlushWAL()
手动刷盘——嵌入场景(如批量导入)会用到,与 sync
正交。
5.2 Group Commit 与 sync 的交互
Group Commit 合并的是 WAL 追加与 MemTable
写入路径,不削弱 sync 语义:若 group 中
任一 writer 请求
sync=true,leader 对整个 group 的 WAL 在
MemTable 写入前做 sync(具体以 group 内
WriteOptions 合并规则为准;实现上 leader 携带
write_options.sync)。
高 QPS + sync=false 时,多条线程的 batch
合并为 fewer fsync 次数的效果主要来自
默认不同步;若每条都
sync=true,Group Commit 仍减少
用户态锁竞争与 WAL 追加 syscall 次数,但
fsync 次数不会低于 sync 请求数。
5.3 recycle_log_files
DBOptions::recycle_log_file_num > 0
时,旧 WAL 文件复用,record 带 log number
区分轮次;此时 WriteOptions::disableWAL
与 recycle 不兼容(WriteImpl
显式校验),因 corruption 检测依赖 sequence 单调性。
六、与 LevelDB / DIY 的对照
| 维度 | LevelDB | RocksDB 9.x |
|---|---|---|
| WAL 格式 | 同 32KB Block + 7B header | + recyclable / compression / UDT record |
| 组提交 | 单 writer _mutex | WriteThread 多 writer 合并 |
| sync | WriteOptions::sync |
同 + MANIFEST WAL 元数据 |
| Batch | WriteBatch | + CF、事务、Wide Column、Entity |
| 并发 MemTable 写 | 无 | allow_concurrent_memtable_write + parallel
memtable writer |
第 2
篇 的 LevelDB DBImpl::Write 可视为本篇的
单子线程、无 CF 子集。
七、实验:exp04 观察 WAL 文件
本系列 reproduce/run_db_bench.sh 的
Exp 04 执行:
cd post/db/rocksdb/reproduce
./build_rocksdb.sh # 或系统 db_bench
export DB_BENCH="$PWD/rocksdb/_build/db_bench" # Makefile 构建则在 rocksdb/ 根目录
./run_db_bench.sh # 仅跑 exp04 时可手动截取脚本中 DB_WAL 段脚本核心参数:
db_bench --benchmarks=fillrandom --num=100000 --value_size=100 \
--db="${DB_WAL}" --statistics=1观察点(不依赖具体机器数字):
${DB}/LOG:可见Syncing log/New WAL file等事件(若开启 info log)。*.log文件:写入期间存在MANIFEST同目录下的 WAL;flush 后可能被 归档或删除(脚本注释:may be recycled after flush)。rocksdb.write-done-by-other等统计:多线程db_bench --threads=N时可对比 Group Commit(需改脚本加--threads)。
性能数字随磁盘与编译选项变化,正文不粘贴未在本机实测的
micros/op;读者运行后查看
output/exp04_fillrandom.txt。
八、Recovery 与 WriteToWAL 细节
DB 打开时 DBImpl::Open →
Recover 路径会:
VersionSet::Recover重放 MANIFEST,得到 LastSequence 与各 CF 文件集;WALManager/log::Reader从min_log_number_to_keep起扫描.log;- 对每条完整 record 解码出
WriteBatch,调用
WriteBatchInternal::InsertInto写入 新的 MemTable(recovery 模式); - 校验 sequence 单调 与 recycle log 的 log number。
WriteToWAL(db/db_impl/db_impl_write.cc)在
leader 路径把 WriteGroup 内各
Writer::batch 按序拼接 写入
log::Writer;若 batch
过大,AddRecord 自动
FIRST/MIDDLE/LAST 分片。WAL
压缩开启时,先可能有
kSetCompressionType
record,再写压缩 payload。
two_write_queues_(WritePrepared
等):非 memtable 写走
nonmem_write_thread_,WAL 与
sequence 在 wal_write_mutex_ 下
ConcurrentWriteToWAL
排序——与默认单队列不同,但
WAL-before-MemTable 不变。
enable_pipelined_write:WAL
阶段与 MemTable 阶段
流水线重叠(PipelinedWriteImpl),提高多核利用率;与
unordered_write、seq_per_batch
等有互斥校验(WriteImpl 开头返回
NotSupported)。
九、工程判读:LOG 与 Statistics
| 信号 | 含义 |
|---|---|
Syncing log #N |
sync=true 或 manual_wal_flush
触发 |
Creating new WAL file |
MemTable switch 或 log 大小达阈 |
NUMBER_WAL_ADD /
WAL_FILE_SYNCED |
tick 计数 sync 行为 |
WRITE_DONE_BY_OTHER |
Group Commit 合并生效 |
Flink checkpoint 慢时,若 LOG 里 WAL sync 与
flush 交替刷屏,需区分是
WriteOptions::sync 还是
writable_file_max_buffer_size
导致的 batch flush——前者在本篇,后者在 Env /
WritableFileWriter 层。
十、小结
RocksDB 写路径的第一步可以概括为:
- WriteBatch 编码多操作、多 CF 的原子批;
- WAL 用 32KB Block + 可分片 record 顺序持久化整批;
WriteImplleader 通过 Group Commit 合并兼容 writer,先 WriteToWAL,再 InsertInto MemTable,最后推进 sequence;sync=true把持久性边界推到fsync+ MANIFEST WAL 记录,与disableWAL互斥组合受严格校验。
下一篇进入 MemTable 与
Flush:SkipList、MemTableList
immutable 队列与 FlushJob 如何把 WAL
已持久化的数据刷成 L0 SST。
参考资料
- RocksDB 9.x
源码:
db/log_format.h、db/log_writer.h、db/log_writer.cc(WAL record 格式与AddRecord)。 - RocksDB 9.x
源码:
include/rocksdb/write_batch.h、db/write_batch.cc(rep_布局与 handler)。 - RocksDB 9.x
源码:
db/db_impl/db_impl_write.cc(WriteImpl、WriteToWAL、sync 路径)。 - RocksDB 9.x
源码:
db/write_thread.h、db/write_thread.cc(JoinBatchGroup、EnterAsBatchGroupLeader)。 - RocksDB 9.x
源码:
db/db_impl/db_impl.h(LogContext)。 - lsm-tree 第 2 篇:WAL + MemTable(DIY record 格式与恢复论证)。
- 本系列 index;实验脚本 reproduce/run_db_bench.sh。
返回 系列目录 | 上一篇:RocksDB 架构演进 | 下一篇:MemTable 与 Flush
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【RocksDB 内核机制】LevelDB 基线:单线程 Leveled 与 DBImpl 骨架
以 LevelDB 1.23 为对照基线:拆解单后台 compaction 线程、无 Column Family 的 DBImpl 结构,以及 Write 队列 group commit、Get 层级查找与 SST/MANIFEST 最小语义,为 RocksDB 9.x diff 地图打底。
【RocksDB 内核机制】Column Family:共享 WAL 与独立 LSM
从 RocksDB 3.0 引入的 Column Family 出发,拆解共享 WAL、独立 MemTable/SST/Version 链、`ColumnFamilyHandle` 生命周期与 DBOptions/ColumnFamilyOptions 分层;并对照 Flink 多 state 变量到 CF 的映射边界。
【RocksDB 内核机制】LevelDB · WAL · Compaction · Column Family · 生产嵌入
补全存储引擎三角最后一角:从 LevelDB 基线与 RocksDB 架构演进,到 WAL/MemTable/SST 写路径、Get/Iterator 读路径、Leveled/Universal compaction 与 write stall,再到 Column Family、事务、Checkpoint 与 Flink/TiKV 嵌入对照。
【RocksDB 内核机制】LSM 生态全景:存储引擎三角与 RocksDB 生态位
闭合存储引擎三角的最后一角:对照 PG B-Tree 与列存 scan,定位 LSM 写优化引擎在 TiKV、Flink、Kafka Streams、ClickHouse Embedded 中的嵌入方式,并划分本系列与 lsm-tree DIY、storage/31–32、stream/12 的分工边界。