土法炼钢兴趣小组的算法知识备份

【RocksDB 内核机制】WAL 与 WriteBatch:持久化与原子批写

文章导航

分类入口
databasestorage
标签入口
#rocksdb#wal#writebatch#group-commit#fsync#lsm-tree#dbimpl#persistence

源码下载

本文相关源码已整理,共 1 个文件。

打开下载目录 →

目录

第 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 Commitsync / disableWALrecyclable logMANIFEST 中 WAL 元数据同步。本文是系列 第 4 篇,只钉写路径入口,不展开 flush(第 5 篇)与 SST(第 6 篇)。

版本锚定:RocksDB 9.x(源码路径以 facebook/rocksdb v9.9.x 为准;本机实验脚本默认 checkout v9.10.0,若 tag 不可用可改 ROCKSDB_TAG)。环境:WSL2 Linux 6.6.x;实验见 reproduce/run_db_bench.sh exp04


一、写路径全景: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 ------>|

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 numberkRecyclableHeaderSize = 11
kSetCompressionType 9 WAL 压缩类型元数据
kUserDefinedTimestampSizeType 10–11 UDT 列族 timestamp 宽度

log::Writer::AddRecorddb/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 路径,本文不展开

原子性边界:一个 WriteBatchWriteImpl 单次 leader 周期 内要么整批进 WAL + MemTable,要么在 WAL 失败时整批不生效(已写 WAL 但未 sync 的边界见下一节)。RocksDB 保证跨两次 Write() 调用的原子性——那是应用层或 TransactionDB 的职责。


四、DBImpl::WriteImpl:Group Commit 与 leader 路径

4.1 入口与校验

DBImpl::Writedb/db_impl/db_impl_write.cc)在 protection_bytes_per_key 等校验后调用 WriteImpl

WriteImpl 先处理特殊模式(two_write_queues_ + disable_memtable 的 WAL-only Prepare、unordered_writeenable_pipelined_write),默认路径为:

  1. 构造 WriteThread::Writer w
  2. write_thread_.JoinBatchGroup(&w) — 链入等待队列或成为 leader;
  3. w.state == STATE_GROUP_LEADER,执行 WAL + MemTable;
  4. 否则等待 leader 完成后返回 w.FinalStatus()

4.2 Group Commit:EnterAsBatchGroupLeader

Leader 在 write_thread_.EnterAsBatchGroupLeader(&w, &write_group)db/write_thread.cc)中从 newest_writer_ 链表 向后扫描,把 WriteOptions 兼容 的 writer 纳入同一 WriteGroup

统计项 WRITE_DONE_BY_OTHERstatistics.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::LogContextdb/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.shExp 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

观察点(不依赖具体机器数字):

  1. ${DB}/LOG:可见 Syncing log / New WAL file 等事件(若开启 info log)。
  2. *.log 文件:写入期间存在 MANIFEST 同目录下的 WAL;flush 后可能被 归档或删除(脚本注释:may be recycled after flush)。
  3. rocksdb.write-done-by-other 等统计:多线程 db_bench --threads=N 时可对比 Group Commit(需改脚本加 --threads)。

性能数字随磁盘与编译选项变化,正文不粘贴未在本机实测的 micros/op;读者运行后查看 output/exp04_fillrandom.txt


八、Recovery 与 WriteToWAL 细节

DB 打开时 DBImpl::OpenRecover 路径会:

  1. VersionSet::Recover 重放 MANIFEST,得到 LastSequence 与各 CF 文件集;
  2. WALManager / log::Readermin_log_number_to_keep 起扫描 .log
  3. 对每条完整 record 解码出 WriteBatch,调用 WriteBatchInternal::InsertInto 写入 新的 MemTable(recovery 模式);
  4. 校验 sequence 单调 与 recycle log 的 log number。

WriteToWALdb/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_writeseq_per_batch 等有互斥校验(WriteImpl 开头返回 NotSupported)。


九、工程判读:LOG 与 Statistics

信号 含义
Syncing log #N sync=truemanual_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 写路径的第一步可以概括为:

  1. WriteBatch 编码多操作、多 CF 的原子批;
  2. WAL 用 32KB Block + 可分片 record 顺序持久化整批;
  3. WriteImpl leader 通过 Group Commit 合并兼容 writer,先 WriteToWAL,再 InsertInto MemTable,最后推进 sequence
  4. sync=true 把持久性边界推到 fsync + MANIFEST WAL 记录,与 disableWAL 互斥组合受严格校验。

下一篇进入 MemTable 与 Flush:SkipList、MemTableList immutable 队列与 FlushJob 如何把 WAL 已持久化的数据刷成 L0 SST。


参考资料

  1. RocksDB 9.x 源码:db/log_format.hdb/log_writer.hdb/log_writer.cc(WAL record 格式与 AddRecord)。
  2. RocksDB 9.x 源码:include/rocksdb/write_batch.hdb/write_batch.ccrep_ 布局与 handler)。
  3. RocksDB 9.x 源码:db/db_impl/db_impl_write.ccWriteImplWriteToWAL、sync 路径)。
  4. RocksDB 9.x 源码:db/write_thread.hdb/write_thread.ccJoinBatchGroupEnterAsBatchGroupLeader)。
  5. RocksDB 9.x 源码:db/db_impl/db_impl.hLogContext)。
  6. lsm-tree 第 2 篇:WAL + MemTable(DIY record 格式与恢复论证)。
  7. 本系列 index;实验脚本 reproduce/run_db_bench.sh

返回 系列目录 | 上一篇:RocksDB 架构演进 | 下一篇:MemTable 与 Flush

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-07 · database / storage

【RocksDB 内核机制】Column Family:共享 WAL 与独立 LSM

从 RocksDB 3.0 引入的 Column Family 出发,拆解共享 WAL、独立 MemTable/SST/Version 链、`ColumnFamilyHandle` 生命周期与 DBOptions/ColumnFamilyOptions 分层;并对照 Flink 多 state 变量到 CF 的映射边界。


By .