RocksDB架构总览

写入路径 · LSM 全图

        客户端 Put(k, v)
              ↓
   ┌─────────────────────────────────────┐
   │ ① WAL append(顺序写)              │  
   │    - 顺序写比随机写快 10x           │
   │    - fsync 策略:3 种场景三选一     │
   └─────────────────────────────────────┘
              ↓
   ┌─────────────────────────────────────┐
   │ ② MemTable.Insert(跳表 O(log N))   │  
   │    - 多层有序链表                   │
   │    - 随机层数代替平衡操作           │
   │    - 无锁并发(CAS 友好)           │
   └─────────────────────────────────────┘
              ↓ 满了 (64MB)
   ┌─────────────────────────────────────┐
   │ ③ 切 Immutable + 新 MemTable        │  
   │    (毫秒级指针切换)                │
   └─────────────────────────────────────┘
              ↓ 后台异步
   ┌─────────────────────────────────────┐
   │ ④ Immutable → 刷盘成 SST → Level 0  │
   └─────────────────────────────────────┘
              ↓
   ⑤ WAL 对应段可删除
              ↓
   ⑥ Compaction 把 L0 整理到 L1+ 

关键节点

WAL

模式 安全性 性能 适用场景
不 fsync 🔴 断电丢秒级数据 ✅✅ 最快(10w+ ops/s) 缓存层、日志聚合
每次 fsync ✅✅ 强一致 🔴 最慢(~100 ops/s 机械盘) 银行交易、金融
批量 fsync 🟡 丢 N ms 数据 ✅ 平衡(1w+ ops/s) 大多数场景

背景知识:

MemTable

需求:支持无锁并发写入 + 范围查找

需求 为什么
有序 刷盘成 SST 时直接顺序写(不再排序)
快速插入 写多读少场景,写要快
快速查找 Get 操作要从 MemTable 先查
范围扫描 Iterator 遍历 key 区间(LSM 标志性能力)
并发友好 多线程写入

核心机制

机制

跳表

核心原理

跳表 = 空间结构换无锁并发

复杂度

操作 复杂度
查找 / 插入 / 删除 O(log N)
范围扫描 O(log N + k)

Immutable 切换机制

「不可变性 + 异步切换」 = 解决并发的银弹

没有 Immutable 会怎样

朴素方案:MemTable 写满 → 直接刷盘 → 清空 → 接受新写入。

问题场景

刷盘耗时几百毫秒到几秒,这期间新写入怎么办?

方案 后果
阻塞新写入,等刷盘完成 🔴 数据库抖动几秒 —— 不可接受
边刷边写同一个 MemTable 🔴 数据竞争 —— 跳表正在被刷盘读,又被新写改

结论

必须把 " 正在写 " 和 " 正在刷盘 "分离

解决什么问题

问题 1:写入不阻塞 ✅

新 MemTable 立刻就绪,用户写入毫无停顿。

问题 2:无锁刷盘 ✅

Immutable 是只读的,刷盘线程可以完全无锁遍历跳表。

MemTable 容量为什么选择 64MB

MemTable 大小 = 内存占用 vs SST 碎片数 的权衡

三档对比

MemTable 1 小时刷出 SST 数 后果
6.4 MB 太小 ~1000 个 🔴 碎片多 → Compaction 累 → 读放大
64 MB ~100 个 ✅ 平衡
640 MB 太大 ~10 个 🔴 内存炸 + P99 抖动 + replay 慢

Compaction 逻辑压力大

Compaction 逻辑:同层多个 SST 重叠就需要合并重写新文件。

读放大
  1. 布隆过滤器:要遍历上千个 SST 的过滤器,CPU 开销大;

  2. 索引:每个 SST 自带索引,要依次查找,磁盘寻道次数变多

  3. 最坏情况:数据分散在几十上百个小 SST,必须全部读取才能确认数据是否存在

    大量小文件 = 多次磁盘 IO = 读放大

1. 内存占用爆炸

每个 SST 都要加载:布隆过滤器、块索引、元数据常驻内存(或按需加载)

2. P99 长尾延迟剧烈抖动

小文件 Compaction 耗时短、分散 IO,不会出现极端毛刺;大文件 Compaction 是 " 大块长时间阻塞 "

3. WAL / 恢复 Replay 回放速度慢(日志太多了)

崩溃恢复时要重放 WAL 构建内存表,刷入 SST:

额外缺陷:单文件损坏影响数据范围极大,修复成本高;范围查询时单次加载超大文件,IO 耗时拉长。

64MB 的好处

维度 64MB 的好处
内存占用 1-2 个 MemTable + Immutable = ~200MB,可接受
刷盘时间 SSD 上几百毫秒,P99 影响小
Level 0 SST 大小 适合后续 Compaction(不大不小)
跳表查找 64MB 数据 ~ 200 万 key,跳表 log N = 21 层,飞快
WAL 大小 64MB WAL 也合适,崩溃 replay 快

启发

任何 buffer 大小的设计,都是 " 过小→碎片 "vs" 过大→延迟 + 内存 " 的权衡。

64MB 是 RocksDB 经过大量生产实践得出的甜点。

Immutable 队列长度

推荐:max_write_buffer_number = 2~3
↓ 理由
- 太少:突发写入立刻 stall
- 太多:内存占用爆炸(每个 64MB × N)
- RocksDB 默认 2

MemTable 何时触发 Switch

3 个触发条件(取最早):
  1. MemTable 写满(如 64MB)
  2. WAL 文件过大(如 1GB → 切 WAL 顺便切 MemTable)
  3. 用户显式 Flush()

MemTable Write Stall · 写入限流

当 Immutable 队列堆积太多 → RocksDB 主动限流写入。

场景

MemTable_active 又满了,但前一个 Immutable 还没刷完盘怎么办?

解决:Immutable 队列

MemTable_active   (可写)
     ↓ 满了
Immutable_2  ← 队尾
Immutable_1  ← 队头(正在刷盘)
     ↓ 刷完
SST 文件

RocksDB 默认允许 2 个 Immutable 同时存在(参数:max_write_buffer_number)。

限流触发

如果 Immutable 队列堆积 >= max_write_buffer_number:
  说明刷盘速度跟不上写入速度
  ↓
🚨 RocksDB 主动 throttle 写入(限流甚至阻塞)
   防止内存爆掉(每个 Immutable 都占内存)

顺序写保证高吞吐

设计哲学

宁愿写入慢一点,也不能让内存爆。
这是自我保护机制。

这是 LSM 比 B+ 树写吞吐高 10x 的根本原因。

三大组件都是顺序写

组件 写法
WAL append-only 顺序写日志
MemTable 内存写(跳表),刷盘时顺序写
SST 顺序写盘(不修改)

顺序写 Vs 随机写

顺序写比随机写:
  机械磁盘 ~ 100 倍快
  SSD     ~ 10 倍快

详见 顺序写vs随机写

附录

P99 长尾