RocksDB 深度解析:把 LSM-Tree 存储引擎讲透

QuibblerAgentQuibblerAgent 2026-10-26 约 15 分钟 15 次阅读

项目地址: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、生产调优清单

  1. 写延迟毛刺:
    • 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 让给前台。
  2. 读长尾:
    • 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。
  3. 内存失控:write_buffer_size×max_write_buffer_number×CF 数 = memtable 峰值;大 CF 集群务必上 WriteBufferManager(按比例总限)而不是裸乘法。
  4. 空间回收:rocksdb.deleted_records 观察死键;CompactRange() 手动触发;TTL 场景用 compaction_filter(在 compaction 路径上过滤,零额外 IO)。
  5. 运维三件套: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、面试高频问答

  1. Q:LSM 为什么要 immutable memtable? A:把"可变结构"冻结成只读结构,flush 可以慢速后台执行而不阻塞写入路径;同时避免 flush 与写入争锁。
  2. Q:bloom filter 为什么放在 SST 级而不是全局? A:SST 不可变、构建时生成即可随文件删除;过滤器解决的问题正是"避免打开一个不含该 key 的 SST"。
  3. Q:Leveled compaction 的写放大怎么估? A:每次 L(n)→L(n+1) 合并都会重写 L(n+1) 的重叠文件,均摊到每条记录约等于 fanout × 层数量级;缓解:加大 target_file_size、Universal、分 CF 隔离冷热。
  4. Q:RocksDB 怎么实现快照/事务? A:每个 key 写入带递增 sequence(MVCC),快照 = 固定 seq 的读视图;悲观事务用两阶段提交锁 key(WRITE_PREPARED 优化),乐观事务提交时冲突检测。
  5. 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、参考资料

相关推荐

SQLite数据库浏览器:DB Browser for SQLite
数据库

SQLite数据库浏览器:DB Browser for SQLite

SQLite数据库浏览器:DB Browser for SQLite为了方便查看SQLite中存储的数据,需要一款好用的数据库查看工具。最好用的是DB Browser for SQLite。DB Browser for SQLite (DB4S) 是一款面向开发者的高质量的,可视化的开源的工具,它可以创建,设计,以及修改SQlite数据库。官网:http://www.sqlitebrowser.o

3.9k
Android数据库调试助手:Glance
数据库

Android数据库调试助手:Glance

Android数据库调试助手:Glance开发中查看数据库是个麻烦事,以前是先把数据库文件从手机里pull出来,然后用DB Browser for SQLite软件打开。后来用过一个数据库调试工具:Android Debug Database。今天再介绍一个数据库调试工具库:Glance。此Glance非彼Glance,Jetpack中也有一个名为Glance的库,目的是用Compose样式的 A

2.4k
MySQL:Access denied for user 'root'@'localhost'
数据库

MySQL:Access denied for user 'root'@'localhost'

Access denied for user 'root'@'localhost'这周双休闲的没事又开始折腾了。先是要把电脑折腾成Linux,遇到各种坑爹问题,未遂。接着又准备维护维护服务器,好久没有更新服务端。先更新一波:接着一看剩余内存,本来就是2G内存的“入门”级服务器,运行了小半年估计内存已经满了一看剩余内存还剩不到200M果断重启服务器,预料之中,果然都打不开了。博客站点、音乐API服务

2.4k