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+
- 详见 Group-Commit 批量Fsync
- 详见 跳表-核心机制
- 详见 #Immutable
- 刷盘耗时几百毫秒到几秒,这期间新写入怎么办?
关键节点
- 写到 ②(MemTable)即可返回成功
- ① WAL 是持久性保证(断电也能恢复)
- ③ 切换是毫秒级(用户无感)
- ④⑤⑥ 都是后台异步(不阻塞用户)
- #顺序写保证高吞吐
WAL
- fsync-三场景决策 fsync 在 rocksdb 支持三种策略
| 模式 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|
| 不 fsync | 🔴 断电丢秒级数据 | ✅✅ 最快(10w+ ops/s) | 缓存层、日志聚合 |
| 每次 fsync | ✅✅ 强一致 | 🔴 最慢(~100 ops/s 机械盘) | 银行交易、金融 |
| 批量 fsync | 🟡 丢 N ms 数据 | ✅ 平衡(1w+ ops/s) | 大多数场景 ⭐ |
背景知识:
MemTable
需求:支持无锁并发写入 + 范围查找
| 需求 | 为什么 |
|---|---|
| 有序 | 刷盘成 SST 时直接顺序写(不再排序) |
| 快速插入 | 写多读少场景,写要快 |
| 快速查找 | Get 操作要从 MemTable 先查 |
| 范围扫描 | Iterator 遍历 key 区间(LSM 标志性能力) |
| 并发友好 | 多线程写入 |
核心机制
- #跳表
- #Immutable 切换机制
- #MemTable 容量为什么选择64MB
- #Immutable 队列长度
- #MemTable何时触发 Switch
- #MemTable Write Stall · 写入限流
机制
跳表
核心原理
跳表 = 空间结构换无锁并发
- 跳表-核心机制
- 跳表-层数随机化:用随机性代替平衡操作
- 跳表-vs红黑树
- 但 RocksDB 内存跳表(memtable skiplist)支持细粒度锁 / 乐观无锁并发写,对比红黑树优势巨大
- 红黑树插入要从根锁住整条路径,粗粒度排他锁,并发很差;
- 跳表插入只锁住沿途少量相邻节点,只阻塞极小局部区间,其他 key 可以并行写入;
- 极致实现可以用乐观锁 + CAS 做到近乎无锁,并发吞吐远高于平衡树。
- 这是它最大工程优势,没错。
复杂度
| 操作 | 复杂度 |
|---|---|
| 查找 / 插入 / 删除 | 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 重叠就需要合并重写新文件。
- 文件越多,层内重叠概率指数上升,频繁触发 Compaction;
- 每次合并要读取、归并、落盘大量小文件,IO 次数暴增;
- 小文件合并产生大量临时 IO,写放大飙升。
读放大
-
布隆过滤器:要遍历上千个 SST 的过滤器,CPU 开销大;
-
索引:每个 SST 自带索引,要依次查找,磁盘寻道次数变多;
-
最坏情况:数据分散在几十上百个小 SST,必须全部读取才能确认数据是否存在。
大量小文件 = 多次磁盘 IO = 读放大。
多
1. 内存占用爆炸
每个 SST 都要加载:布隆过滤器、块索引、元数据常驻内存(或按需加载)
-
单个 640MB SST 的索引、过滤器体积远大于 64MB;
-
少量大文件也会把 block cache、过滤器缓存占满,挤占业务缓存;
内存不足时会频繁把索引刷下磁盘,查询时又要重新加载,性能暴跌。
2. P99 长尾延迟剧烈抖动
-
#P99 长尾
Compaction 触发时要合并巨型文件: -
单次 Compaction 需要读取、归并、写入数百 MB 数据;
-
短时间占用大量磁盘带宽、CPU;
-
正常读写请求被 IO 抢占,出现大量慢请求,P99、P999 延迟飙升。
小文件 Compaction 耗时短、分散 IO,不会出现极端毛刺;大文件 Compaction 是 " 大块长时间阻塞 "。
3. WAL / 恢复 Replay 回放速度慢(日志太多了)
崩溃恢复时要重放 WAL 构建内存表,刷入 SST:
- 要攒足 640MB 才会刷盘,memtable 会堆积大量数据;
- 崩溃后需要回放海量日志才能重建完整 SST,恢复耗时成倍增加;
- 写入吞吐波动大:长时间累积数据,一次性刷盘产生瞬时 IO 洪峰。
额外缺陷:单文件损坏影响数据范围极大,修复成本高;范围查询时单次加载超大文件,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 长尾
-
正常查询:1ms、2ms、3ms
-
99% 请求都在 5ms 以内
-
但有 1% 请求要 50~200ms
那 P99 = 50ms,这个巨大差值就是长尾。