项目地址:https://github.com/facebook/rocksdb 定位:Facebook 基于 LevelDB 演化的高性能嵌入式 KV 存储引擎,是当代分布式系统底座的事实标准之一。
| 元信息 | 说明 |
|---|---|
| 语言 | C++(核心)+ 多语言 binding |
| 星标 | 27k+ ⭐(以仓库实时为准) |
| 协议 | Apache 2.0 / GPLv2 双许可 |
| 谁在用 | TiKV(TiDB)、Flink 状态后端、MyRocks(MySQL)、Cassandra、CockroachDB(早期)、大量日志/时序系统 |
1、B-Tree 的写入瓶颈
- B-Tree(InnoDB、SQLite)每次插入本质是"页内随机写 + 可能的页分裂",磁盘 IOPS 与随机写放大是瓶颈;
- LSM 的思路(1996, O'Neil et al.):先在内存攒一批,再顺序写成一个不可变文件(SST),旧文件后台慢慢合并。随机写 → 顺序写,这是存储系统最重要的性能变换。
RocksDB 相对 LevelDB 的主要增强:列族、多线程 compaction、丰富的压缩策略、bloom filter 全键/前缀、rate limiter、事务、备份、背压(WriteBufferManager)、Pipelined Write 等。
2、一次写入的旅程
Put(k,v)
│
▼
[可选] WriteBatch / WAL(顺序追加,崩溃安全)─────────────┐
│ │ 同步策略:
▼ │ WRITE_AHEAD /
Active Memtable(默认 SkipList) │ WRITE_PREPARED 等
│ 写满 write_buffer_size(默认 64MB) │
▼ 变为 ▼
Immutable Memtable ──(Flush 线程)──> SST Level-0(key 范围可重叠)
│ 后台 Compaction
▼
SST Level-1..N(层内 key 范围不重叠)
│
▼
磁盘文件(*.sst,带指纹、索引、footer)
2.1、Memtable 为什么用跳表
- 默认
memtable_factory = SkipListFactory:无锁并发读 + 单写者/并发写(allow_concurrent_memtable_write); - 可换 HashSkipList / HashLinkList / VectorRep:把"点查"复杂度压到 O(1),牺牲范围扫描——按 workload 选择数据结构是存储引擎的日常决策。
2.2、SST 布局与配套文件
┌──────────────────────────── SST 文件 ────────────────────────────┐
│ Data Block │ Data Block │ ... │ Meta Block(bloom) │ Index Block │ Footer │
└──────────────────────────────────────────────────────────────────┘
- Data Block:有序 KV,前缀压缩 + restart point(定期存全 key,支持二分)
- Index Block:key range → block 偏移
- Meta Block:bloom filter(whole key / prefix,SST 级别)
- Footer:指向上面的指针 + 魔数,反向解析入口
配套组件:
- MANIFEST:记录版本集合(VersionSet)——哪个 SST 属于哪一层、每层文件清单。compaction 产生新文件后先写 MANIFEST 再切换,保证崩溃后能恢复一致的"视图";
- CURRENT:指向当前 MANIFEST 文件;
- WAL:memtable 的回放日志,按与 memtable 同序追加。
2.3、一次读取:四道漏斗
Get(k)
1. Active/Immutable Memtable(内存,命中即返回,带 sequence 判断新版本)
2. Block Cache(LRU,可分 HighPri/MidPri 通道;默认 32MB)
3. 逐层定位 SST:MANIFEST → 文件范围 → bloom filter(负相关 99%+ 拦截,1 次内存运算换 1 次磁盘 IO)
4. SST 内:Index Block → Data Block → 二分 restart point
读放大的本质:L0 内文件互相重叠,最坏要查 L0 全部文件 + L1..Ln 各一个文件。这就是 L0 触发 compaction 阈值(level0_file_num_compaction_trigger,默认 4)如此敏感的原因。
2.4、Compaction:三放大调节阀
| 策略 | 思路 | 适合 |
|---|---|---|
| Leveled(默认) | Ln 触发后把文件与 Ln+1 重叠部分合并;层容量按 fanout(默认 ×10)增长 | 通用、读放大低、写放大高 |
| Universal(size-tiered) | 同尺寸小文件攒一批合并成大文件 | 写多读少、大 value |
| FIFO | 只保留最近 N 个文件,旧文件直接删(按 TTL 滚动) | 时序/缓存类数据 |
三放大在 Leveled 下的直觉:
- 写放大(WA)≈ 1×fanout×层数量级:同一份数据在逐层下移中被反复重写;
- 读放大(RA)≈ L0 文件数 + 层数(bloom 能大幅缓解点查);
- 空间放大(SA)≈ 旧版本数据在被 compaction 合并前一直占空间(默认约 10%)。
工程上的第一定律:没有免费午餐——调 WA 必然动 RA/SA。 例如调大 target_file_size 与层容量 → WA 降、SA 升;开 Universal → WA 降、RA/SA 升。
2.5、列族:引擎里的多张表
- 多 CF 共享 WAL(保证跨 CF 原子写)但各有一套 memtable/SST;
- 典型用法:TiKV 把 raft 日志、数据、锁放不同 CF,分别设置压缩参数(比如锁 CF 用
kNoCompression+ 小 buffer,及时释放); WriteBatch天然跨 CF 原子。
3、快速上手
git clone https://github.com/facebook/rocksdb.git
cd rocksdb && make -j8 shared_lib # 或 DEBUG_LEVEL=0 优化编译
#include "rocksdb/db.h"
int main() {
rocksdb::DB* db; rocksdb::Options o;
o.create_if_missing = true;
o.IncreaseParallelism(); o.OptimizeLevelStyleCompaction();
rocksdb::Status s = rocksdb::DB::Open(o, "/tmp/testdb", &db);
s = db->Put(rocksdb::WriteOptions(), "key1", "value1");
std::string v; s = db->Get(rocksdb::ReadOptions(), "key1", &v);
// 迭代器(快照隔离的 MVCC 读)
rocksdb::Iterator* it = db->NewIterator(rocksdb::ReadOptions());
for (it->Seek("k"); it->Valid() && it->key().ToString() < "l"; it->Next()) {}
delete it; delete db;
}
诊断工具:ldb(ldb scan /tmp/testdb、ldb manifest_dump)、sst_dump(查看单 SST 与 bloom 命中)、db_bench(基准压测)。
4、生产调优清单
- 写延迟毛刺:
- WAL:
WriteOptions::sync=false+ 上层保证(如 raft 已有持久日志)时吞吐最高;需要自身持久则sync=true; options.level0_slowdown_writes_trigger / stop_writes_trigger:compaction 跟不上会直接背压写入——监控LEVEL0_SLOWDOWN_TOTAL与WRITE_STALL;rate_bytes_per_sec(rate limiter):给 compaction 限速,把 IO 让给前台。
- WAL:
- 读长尾:
block_cache给足(内存预算的 30-50%),开启cache_index_and_filter_blocks=true防止索引常驻挤爆内存(注意与 pin 匹配);- bloom
bloom_locality/bits_per_key=10(≈1% 误判);前缀查询用prefix_extractor+ReadOptions::prefix_same_as_start。
- 内存失控:
write_buffer_size×max_write_buffer_number×CF 数 = memtable 峰值;大 CF 集群务必上 WriteBufferManager(按比例总限)而不是裸乘法。 - 空间回收:
rocksdb.deleted_records观察死键;CompactRange()手动触发;TTL 场景用compaction_filter(在 compaction 路径上过滤,零额外 IO)。 - 运维三件套:
GetProperties拿 compaction pending 字节、DeleteObsoleteFilesPeriod、备份用BackupEngine(一致性快照)而不是 rsync 数据目录。
5、选型对比
| 引擎 | 结构 | 优劣 |
|---|---|---|
| RocksDB | LSM | 写吞吐极高、可调性极强;RA/SA 与运维复杂 |
| LMDB | B+Tree(COW) | 读极快、零拷贝、崩溃安全;写并发与空间回收弱 |
| SQLite | B-Tree | 嵌入式关系库、生态最好;高并发写弱 |
| Pebble(Go) | LSM,CockroachDB 自研 | RocksDB 的 Go 复刻,云原生存储协程化 |
6、面试高频问答
- Q:LSM 为什么要 immutable memtable? A:把"可变结构"冻结成只读结构,flush 可以慢速后台执行而不阻塞写入路径;同时避免 flush 与写入争锁。
- Q:bloom filter 为什么放在 SST 级而不是全局? A:SST 不可变、构建时生成即可随文件删除;过滤器解决的问题正是"避免打开一个不含该 key 的 SST"。
- Q:Leveled compaction 的写放大怎么估? A:每次 L(n)→L(n+1) 合并都会重写 L(n+1) 的重叠文件,均摊到每条记录约等于 fanout × 层数量级;缓解:加大 target_file_size、Universal、分 CF 隔离冷热。
- Q:RocksDB 怎么实现快照/事务? A:每个 key 写入带递增 sequence(MVCC),快照 = 固定 seq 的读视图;悲观事务用两阶段提交锁 key(WRITE_PREPARED 优化),乐观事务提交时冲突检测。
- Q:为什么 Flink 用 RocksDB 做状态后端? A:超大状态(TB 级)+ 高频写 + 需要迭代器做窗口/聚合,LSM 的顺序写特性与可序列化 KV 契合;且通过限制 CF/迭代器复用来控制读放大。
7、总结
RocksDB 把"存储引擎"抽象成一组可组合的旋钮:memtable 结构、compaction 策略、过滤器、缓存、背压。读懂它,TiDB/TiKV、Flink、Kafka Streams 的性能问题就有了共同语言。建议路线:先用 db_bench 复现"三放大"曲线 → 读 db_impl_write.cc / memtable.cc / version_set.cc 三个核心文件 → 动手调一个真实业务 workload。
8、参考资料
- 官方文档与 blog:https://github.com/facebook/rocksdb/wiki
- O'Neil et al., The Log-Structured Merge-Tree(1996)
- 《Database Internals》Part II: Distributed / B-Tree vs LSM 章节
- RocksDB 中文资料:https://github.com/0voice/rocksdb_doc(社区整理)


