原文发布于 quant67.com,转载请保留出处。
第 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 提交:
| 阶段 | 源码锚点 | 持久化? |
|---|---|---|
| 组批 | 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 对齐
text
+-----+-------------+--+----+----------+------+-- ... ----+
| 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_ 布局(节选):
text
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 很小( ≤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):
cpp
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 单调性。
5.4 文献与工程间隙:WAL 从哪来
| 来源 | 类型 | 本文引用点 |
|---|---|---|
| Gray & Reuter, Transaction Processing ;Mohan et al., ARIES (TODS 1992) | A 级经典 | 先日志、后数据页 的崩溃恢复范式;LSM 把「页」换成 MemTable+SST |
| O'Neil LSM(1996) | A 级 | WAL 是 写路径顺序化 的第一环(第 1 篇) |
LevelDB log_writer / RocksDB 同源 |
A 级源码 | 32KB block + record 分片;Group Commit 在 WriteThread 上扩展 |
工程间隙 :ARIES 讨论 fsync 与组提交 的吞吐--持久性权衡;云盘与 NVMe write cache 下 sync=true 的 tail 与论文实验室 SSD 不可直接比 。Flink checkpoint 前的 同步 Flush 叠加 WAL 写,stall 信号可能来自 MemTable 队列而非 WAL 本身(第 16 篇)。
开放问题 :能否用 group commit + 自适应 sync 在 tail 可控下接近 sync=false 吞吐?工业侧多靠业务分级写 WriteOptions,无引擎内统一最优策略。
六、与 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 执行:
bash
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 段
脚本核心参数:
bash
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 路径)。 - Mohan, C. et al., ARIES: A Transaction Recovery Method..., TODS 1992(WAL 恢复范式;A 级)。
- O'Neil et al., LSM-Tree, Acta Informatica 1996(顺序写 + 日志;A 级)。
- lsm-tree 第 2 篇:WAL + MemTable(DIY record 格式与恢复论证)。
- 本系列 index;实验 reproduce/run_db_bench.sh exp04。
返回 系列目录 | 上一篇:RocksDB 架构演进 | 下一篇:MemTable 与 Flush