第
4 篇 钉住了 WAL + WriteBatch
如何把写持久化并原子进内存。MemTable 是 LSM 写路径的
吸收层:随机写先落内存结构,攒够
write_buffer_size 后
冻结(immutable),由后台
FlushJob 顺序扫描输出 L0
SST。
lsm-tree 第 2
篇 用 C 实现了跳表 MemTable 与 WAL 恢复;本文对照
RocksDB 9.x 生产实现:默认
SkipList
MemTableRep、MemTableList 多
immutable 队列、FlushJob 与
BuildTable 的协作。Flush 触发的 write
stall 留 第 11
篇;SST 内部 Block 布局留 第 6
篇。
版本锚定:RocksDB 9.x(
memtable/skiplist.h、db/flush_job.*、db/memtable_list.*)。实验:reproduce/run_db_bench.shexp05(write_buffer_size=65536强制频繁 flush)。
一、MemTable 在 LSM 写路径中的位置
flowchart TB
W["WriteImpl: InsertInto"] --> AM["Active MemTable<br/>(mutable)"]
AM -->|"size ≥ write_buffer_size"| SW["SwitchMemtable"]
SW --> IM["MemTableList<br/>immutable 队列"]
IM --> FJ["FlushJob (bg thread)"]
FJ --> SST["L0 *.sst"]
SST --> VS["VersionSet::LogAndApply"]
W --> WAL["WAL .log"]
| 结构 | 可写? | 读者可见? | 生命周期 |
|---|---|---|---|
| Active MemTable | 是(write thread / 并发 insert) | 是(SuperVersion) | 直到 SwitchMemtable |
| Immutable MemTable | 否 | 是 | 直到 FlushJob 完成并 Unref |
| L0 SST | 否 | 是 | MANIFEST 管理,compaction 回收 |
与 WAL 的关系:WAL 保证 进程崩溃 时 MemTable 未 flush 部分可重放;MemTable flush 成功后,对应 WAL 段可在 min_log_number_to_keep 规则下回收(与 MANIFEST 中 log number 联动,见第 6 篇)。
二、SkipList MemTableRep
2.1 为何用跳表
RocksDB 默认
Options::memtable_factory = SkipListFactory()。底层模板
memtable/skiplist.h 继承
LevelDB 设计:
- 写:外部同步(write thread 或 DB mutex);插入 \(O(\log n)\);
- 读:无内部锁;依赖 节点链接后 immutable 的不变式;
- 参数:
max_height = 12,branching_factor = 4(构造函数默认)。
db/memtable.h 注释概括 CF 侧模型:
For each CF, rocksdb maintains an active memtable that accept writes, and zero or more sealed memtables (immutable). MemTableList maintains a list of
ReadOnlyMemTableobjects.
2.2 InternalKey 与 Arena
MemTable 存的不是用户 key,而是
InternalKey(db/dbformat.h):user_key + sequence + value_type。同一
user key 的多版本按 sequence 降序 排列,供
Get / Iterator 找最新可见版本(第 7
篇)。
内存分配走 Arena /
ConcurrentArena(allow_concurrent_memtable_write
时),减少小块
malloc;ImmutableMemTableOptions::arena_block_size
等由 MutableCFOptions 注入。
2.3 可选 MemTableRep
通过 MemTableRepFactory 可换
HashSkipList、VectorRep(bulk
load)、Ribbon 等。默认 SkipList 在
通用随机写 与 范围扫描
之间平衡最好;Flink state 等点查为主场景有时调
prefix_extractor + Hash 结构(第
16 篇)。
2.4 与 lsm-tree DIY 对照
| 维度 | lsm-tree wal-memtable | RocksDB 9.x |
|---|---|---|
| 跳表 | 手写 C SkipList | memtable/skiplist.h +
MemTableRep 抽象 |
| Key | 简化 InternalKey | 完整 sequence/type/UDT/blob index |
| Immutable | 单块切换 | MemTableList 多槽 +
max_write_buffer_number |
| Flush | 系列后续篇 | FlushJob + BuildTable +
多线程调度 |
三、MemTableList:mutable 与 immutable 队列
3.1 两层结构
db/memtable_list.h 定义:
MemTableListVersion:不可变快照,持std::vector<ReadOnlyMemTable*>,从新到旧 搜索(Get/AddIterators);MemTableList:维护当前 Version 指针、refcount;切换时 COW 新版本。
读路径通过 SuperVersion
持有 MemTableListVersion* 指针,避免 flush
期间长时间持锁(Install 时原子切换)。
3.2 容量与
max_write_buffer_number
MutableCFOptions 相关项:
| 选项 | 作用 |
|---|---|
write_buffer_size |
单个 MemTable 软上限;达阈触发 SwitchMemtable |
max_write_buffer_number |
immutable + active 总个数上限;超出 stall 写 |
min_write_buffer_number_to_merge |
flush 时可合并多个 immutable 一次写 SST |
双缓冲直觉:active 写满 → 变 immutable 并新建 active;后台 flush 最旧 immutable 时不阻塞新写(除非 stall)。
3.3 Get 与 Iterator 如何叠层
MemTableListVersion::Get(注释):从
最新 MemTable 向旧搜索,返回
第一个 匹配 key 的版本,并通过
*seq 带回 sequence。
Iterator 路径 AddIterators 为 每个
immutable + active 构造
MemTableIterator,再与 SST 层
MergeIterator 合并(第 8
篇)。
3.4 Memtable history(可选)
max_write_buffer_number_to_maintain /
max_write_buffer_size_to_maintain 可在 flush 后
保留已 flush MemTable 只读副本 于
history,供 Transaction 冲突检测等;与
GetFromHistory 配合。默认 OLTP 负载可忽略。
四、SwitchMemtable:从 mutable 到 immutable
Switch 由
DBImpl::SwitchMemtable(db/db_impl/db_impl_write.cc
等)在 write 路径或 flush 压力下调度,典型步骤:
- 将当前 active 标记为
immutable(
MemTable::MarkImmutable+ 不再接受 insert); - 分配新 active MemTable(新 ID / arena);
MemTableList::Add把 immutable 挂入列表;- 若 WAL 需要切换,新建 log
文件(
log::Writer新log_number); imm()->FlushRequested()→EnqueuePendingFlush→MaybeScheduleFlushOrCompaction。
Atomic
flush(atomic_flush=true):多 CF
同时 Switch 并共享 flush 边界,保证 CF 间
checkpoint 一致——Flink 多 CF state 场景会关心(第 13、16
篇)。
五、FlushJob:PickMemTable 到 L0
5.1 生命周期
db/flush_job.h FlushJob 核心
API:
| 方法 | 调用线程 | 作用 |
|---|---|---|
PickMemTable() |
持 db_mutex |
从 MemTableList 选取待 flush 的
memtable(s) |
Run() |
bg flush 线程 | 写 SST + 可选 Install 到 Version |
Cancel() |
关闭 / 错误 | 放弃 flush |
构造参数包含:ColumnFamilyData*、VersionSet*、snapshot
列表(决定 flush 时保留的最老
sequence)、FlushReason、output_compression
等。
5.2 WriteLevel0Table
FlushJob::Run 在 mempurge 等分支之后调用
WriteLevel0Table()(db/flush_job.cc):
- 合并
mems_(一个或多个 immutable MemTable)上的 按 InternalKey 排序 迭代器; - 调用
BuildTable(db/builder.cc)+BlockBasedTableBuilder输出.sst; - 填充
FileMetaData(file number、smallest/largest key、sequence 范围、checksum); - 通过
VersionEdit::AddFile准备 MANIFEST 增量。
Flush 不 直接删 MemTable:Install 成功后
MemTableList::Remove 并
Unref;失败则 rollback 或保留待重试。
5.3 FlushReason 与调度
FlushReason
枚举包括:kWriteBufferFull、kWriteBufferManager、kManualCompaction、kExternalFileIngestion
等。DBImpl::BackgroundFlush 从
flush_queue_ 取
FlushRequest,线程池
Env::Priority::HIGH 通常优先于
compaction(可配置)。
WriteBufferManager(全局内存 cap)可在 未达单 CF write_buffer_size 时提前 flush,避免进程 RSS 失控——与 stream-processing 第 12 篇 的 state 内存调参相关。
5.4 MemPurge(实验特性)
FlushJob 含 MemPurge
路径:对 overwrite 重的负载 过滤 immutable 中已失效
KV 到新 MemTable,减少写放大;失败则回退普通
flush。生产默认仍以 全量 sort-merge 写 SST
为主路径。
六、Install 与 WAL 回收衔接
Flush Install
阶段(VersionSet::LogAndApply)把 新 L0
文件 写入 MANIFEST,并更新
Version 层级视图。成功后:
- 只被已 flush MemTable 覆盖的 WAL 段可标记删除;
GetLiveFiles/ checkpoint 以 MANIFEST + SST 为准。
读路径此时可见 MemTable(尚未 flush 的新写)+ 新 L0 + 旧 Ln 叠加;SuperVersion 切换保证 原子视图(第 7 篇)。
七、实验:exp05 强制 flush 观察 SST
reproduce/run_db_bench.sh Exp
05:
cd post/db/rocksdb/reproduce
export DB_BENCH="$PWD/rocksdb/db_bench" # 或 _build/db_bench
# 脚本片段:
db_bench --benchmarks=fillrandom --num=50000 --write_buffer_size=65536 \
--db="${DB_FLUSH}"
ls "${DB_FLUSH}"/*.sst | wc -l设计意图:write_buffer_size=65536(64
KiB)远小于 5 万条 ×(key + 100B value),触发 多次
SwitchMemtable + flush,DB 目录下出现 多个
.sst。
观察点:
- SST 个数随写入递增(exact 数字取决于 key/value 大小与 block size,不在此伪造)。
LOG中(Started/Finished) flush事件与event:flush_finishedJSON(若run_properties.sh抓取)。- 对比 默认 64 MiB
write_buffer_size:SST 个数显著减少——说明 flush 频率由 MemTable 容量驱动。
多线程 fillrandom 可叠加观察
max_write_buffer_number 触发的
stall(需更低阈值,见第 11 篇 exp11)。
与 WAL 目录对照:同一 DB 在 flush 后执行
ls *.log 与 ls *.sst——WAL 文件数与
max_total_wal_size /
wal_ttl_seconds 回收策略相关;SST
文件数直接反映 MemTable flush
次数。二者比值是排查「写放大在 WAL 还是
L0」的第一眼线索。
八、并发 MemTable 写入
当
DBOptions::allow_concurrent_memtable_write=true
且 MemTableRep 支持时,WriteImpl leader 在 WAL
之后可把 follower 设为
STATE_PARALLEL_MEMTABLE_WRITER,多线程并行
MemTable::Insert。
约束(db/db_impl/db_impl_write.cc
注释):
- MemTableRep 必须声明支持并发写;
- batch 含 Merge 时退化为串行;
inplace_update_support与并发写不兼容(启动时CheckConcurrentWritesSupported检查)。
并行写后通过
MemTablePostProcessInfo
批量更新 num_entries / data_size
统计,避免热路径 atomic 风暴。对 高线程
db_bench,开并发 MemTable 写可显著降
write_memtable_time——但 CPU 与 cache
争用上升,需实测。
九、Flush 调度与 PickMemTable
DBImpl::BackgroundFlush 循环从
flush_queue_
取任务。FlushJob::PickMemTable(持
db_mutex)逻辑要点:
- 从
ColumnFamilyData::imm()(即MemTableList)取 最旧 且memtable_id ≤ max_memtable_id的 immutable; - 若
min_write_buffer_number_to_merge > 1,尝试 合并相邻 immutable 一次 flush,减少 L0 文件数; - 记录
existing_snapshots:flush 输出的 SST 必须保留 snapshot 仍需要的旧 sequence(通过SnapshotChecker/earliest_write_conflict_snapshot); PickMemTable之后必须Run()或Cancel(),否则泄漏 memtable 引用。
FlushJob::Run 成功且
write_manifest=true
时,Install 调用
VersionSet::LogAndApply;失败则
Rollback immutable
回列表或触发 BGError。
十、FileMetaData 与 L0 特殊性
Flush 产出
FileMetaData(db/version_edit.h
相关结构)字段包括:
| 字段 | 用途 |
|---|---|
fd.GetNumber() |
SST 文件名中的 file number |
smallest / largest |
InternalKey 边界 |
smallest_seqno /
largest_seqno |
覆盖的 sequence 范围 |
marked_for_compaction |
是否需要优先 compaction |
temperature /
epoch_number |
分层存储与 compaction 排序(9.x 扩展) |
L0 文件 key range 可重叠( unlike L1+ 层间不变式)。故 L0 读放大高,需 L0→L1 compaction 合并(第 10 篇)。频繁小 flush(exp05)会 堆 L0 文件,是故意演示 trade-off,而非推荐配置。
十一、调参直觉(非 cookbook)
| 现象 | 可能旋钮 | 机制 |
|---|---|---|
| L0 文件过多 | 增大 write_buffer_size / 降低 flush
频率 |
每个 MemTable 对应潜在 1 个 L0 |
| 写 stall | 增大 max_write_buffer_number 或 memtable
总内存 |
immutable 队列满 |
| Flush 抢 I/O | max_background_flushes、RateLimiter |
bg flush 与 compaction 共享磁盘 |
| 内存峰值 | WriteBufferManager 全局上限 |
提前 flush 换内存 |
详细参数矩阵见 storage/32-rocksdb-practice;本系列只解释 内核为何读这些旋钮。
db_bench 对照实验:在 exp05
基础上把 write_buffer_size 改为默认
67108864(64 MiB)再跑 5 万条,对比 SST 个数与 flush
次数——同一 num 下 SST 个数应
显著减少,说明 flush 频率主要由 MemTable
容量驱动而非绝对写入条数。
11.1 Range tombstone 与 flush
DeleteRange 在 MemTable 中存 range
deletion meta,flush 时写入 SST 的 range
tombstone block(或 meta 索引),读路径由
RangeDelAggregator 合并
MemTable 与 SST 层 tombstone(第 8
篇)。大 range delete 会 放大 MemTable
元数据,可能触发 提前 flush——排查
delete-range 性能时不只看 compaction,也要看 MemTable
占用。
11.2 imm() 与
SuperVersion
ColumnFamilyData::imm() 返回
MemTableList*,与
SuperVersion 中的
memtable_list 指针配合:Install flush 时
新建 SuperVersion,读线程持旧 SuperVersion
ref 仍看到 flush 前 MemTable,写线程在新 SuperVersion
上继续。理解 immutable 队列长度 必须同时看
imm()->NumNotFlushed() 与
stall 条件——仅看 RSS 不够。
Flush 线程优先级默认
Env::Priority::HIGH(相对
LOW compaction),意图是
尽快释放 immutable 占用的
write_buffer_size,避免写
stall。若磁盘为 HDD,过高 flush 并发可能与 compaction
争抢 seek,需结合
max_background_flushes 与
max_background_jobs 联调(第
12 篇)。
与 stream-processing 的衔接:Flink RocksDB state backend 的增量 checkpoint 依赖 flush 产出的 SST 文件列表;MemTable 越大,checkpoint 间 未 flush 数据越多,恢复 replay WAL 越长——embed 场景常在 更小 write_buffer 与 更频繁 flush 之间取舍,而非无脑放大 MemTable。
十二、小结
MemTable 层完成三件事:
- SkipList(默认) 提供有序、并发友好的内存表;
MemTableList管理 active + 多个 immutable,读路径按时间从新到旧叠层;FlushJob在后台把 immutable 顺序刷为 L0 SST,经VersionEdit进入 MANIFEST。
写路径链条至此:WriteBatch → WAL → MemTable → SST → MANIFEST 全部连通。下一篇拆解 SST 文件格式 与 MANIFEST / VersionSet 的磁盘语义。
参考资料
- RocksDB 9.x
源码:
memtable/skiplist.h(跳表不变式与并发读)。 - RocksDB 9.x
源码:
db/memtable.h、db/memtable.cc(MemTable / ReadOnlyMemTable)。 - RocksDB 9.x
源码:
db/memtable_list.h、db/memtable_list.cc(immutable 队列与Get)。 - RocksDB 9.x
源码:
db/flush_job.h、db/flush_job.cc(PickMemTable、WriteLevel0Table)。 - RocksDB 9.x
源码:
db/builder.cc(BuildTable)。 - RocksDB 9.x
源码:
db/db_impl/db_impl_write.cc(SwitchMemtable、EnqueuePendingFlush)。 - lsm-tree 第 2 篇:WAL + MemTable(DIY 跳表与 WAL 恢复)。
- 本系列 index;实验 reproduce/run_db_bench.sh exp05。
返回 系列目录 | 上一篇:WAL 与 WriteBatch | 下一篇:SST 格式与 MANIFEST
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【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 的分工边界。
【RocksDB 内核机制】WAL 与 WriteBatch:持久化与原子批写
从 log::Writer 的 32KB Block 分片 record、WriteBatch 二进制布局,到 DBImpl::WriteImpl 中 Group Commit 与 sync/fsync 语义,钉住写路径第一步:先 WAL 后 MemTable 的提交顺序与原子性边界。
【RocksDB 内核机制】SST 格式与 MANIFEST:BlockBasedTable 与 VersionSet
从 BlockBasedTable 的 Data/Index/Filter Block 与 Footer 定界,到 VersionEdit 增量记录与 VersionSet::LogAndApply 如何维护 LSM 快照,并用 sst_dump 对照磁盘布局。