假设一个团队在做边缘侧的服务,主语言是 Go。元数据、任务游标、去重表都放在一个嵌入式存储里。两年前选型时选了 RocksDB,理由很朴素:成熟,性能口碑好,用的人多。服务在 x86 容器里跑了一年多,存储层面没出过问题。
转折发生在一次交付。客户要求同一份代码跑在一款 ARM 单板上,二进制要静态链接,现场没有包管理器。团队开始折腾 cgo 的交叉编译,工具链、系统库版本、链接顺序,一样样试过去。两周过去,构建仍然只在开发机上成功。有人提议换个纯 Go 的存储,有人说 LMDB 是纯 C 的库,静态链接应该省事,也有人说别动,把构建问题啃下来。
三个方向都能走通,区别在于各自要付什么账。这篇想把账目摊开:先看 RocksDB 到底提供了什么,再看 Pebble 和 LMDB 各自换掉了什么,最后回到那个最容易被跳过的问题,这次到底该不该换。
RocksDB 提供的,以及它一起带来的
RocksDB 由 Facebook 的数据库工程团队开发维护,建立在 Google 的 LevelDB 之上,后者出自 Sanjay Ghemawat 和 Jeff Dean 的工作。它是一个嵌入式、持久化的键值存储库,没有独立服务进程,直接链接进应用。公共接口放在 include/ 目录,内部头文件可能不经通知就变。
它的存储结构是 LSM,Log-Structured-Merge。写入先进入内存结构和日志,之后刷成有序文件,后台靠 compaction 把这些文件一层层合并。这套结构允许在写放大、读放大、空间放大之间做权衡:合并得勤,读的时候要翻的层数少,写的时候额外搬运的数据多;合并得懒,写入省事,读就得多找几层。它支持多线程 compaction,设计上能在一个库里放数 TB 数据,官方也强调它尤其适合闪存盘。
这些能力是用复杂度换来的。参数多,compaction 策略要选,内存预算要调,出问题时需要看得懂 LSM 的分层状态。对一个只存几十 GB 元数据、读多写少的服务来说,这些旋钮大多数时候在空转,构建负担却一点不少:cgo、C++ 工具链、平台相关的编译选项,每换一个目标平台都要重新过一遍。
关于授权,很多人担心错了地方
法务问起授权,是嵌入式存储选型里常见的一幕。RocksDB 用的是 GPLv2 与 Apache 2.0 双授权,使用者可以任选其一。这一条很关键:选了 Apache 2.0,就不必按 GPL 的传染性条款去处理自己的代码。所以如果唯一的顾虑是”RocksDB 会不会污染我们的闭源代码”,答案通常是不会,前提是团队明确选择 Apache 2.0 并在合规记录里写清楚。
真正难处理的是另外两件事。一是构建与分发:静态链接之后,二进制里要带上一大块 C++ 代码,体积和需要审计的依赖都会增加。二是认知成本:一个 Go 团队要维护一套 LSM 的调参经验,人员流动时这部分知识很难交接。
把授权放回它该在的位置,后面的比较才有意义。
Pebble,同一套文件格式,换一种语言
Pebble 是 CockroachDB 团队写的 Go 语言键值数据库,受 LevelDB 和 RocksDB 启发,目标很明确:性能,以及服务 CockroachDB 自己的需求。它继承了 RocksDB 的文件格式,还有几处扩展,比如 range deletion tombstones、表级 bloom filter、MANIFEST 格式的更新。
它有意不实现 RocksDB 的全部功能。已经实现的部分覆盖 block-based tables、checkpoints、indexed batches、迭代器选项、level-based compaction、手动 compaction、merge operator、前缀 bloom filter、前缀迭代、范围删除、反向迭代、SSTable 导入、single delete、快照、表级 bloom filter。没有实现的部分包括备份、column families、范围内删文件、FIFO compaction、forward 与 tailing 迭代器、哈希表格式、memtable bloom filter、持久化缓存、pin 迭代器的键值、plain table 格式、SSTable ingest-behind、sub-compactions、事务、universal compaction。
这份清单值得逐条对着自己的用法看一遍。假设一个团队的业务里用 column families 做逻辑隔离,或者用备份接口做定期快照,那 Pebble 缺的就是承重的一块,没法当作可有可无的小功能。
Pebble 相对 RocksDB 也做了不少改进,官方列出的有:memtable 跳表加了反向链接,反向迭代更快;提交流水线更快,并发表现更好;indexed batch 与迭代可以无缝合并;L0 sublevels 与 flush splitting;用 copy-on-write B 树保存文件元数据;delete-only compaction;block-property collectors 与 filters;range keys API;代码库更小、更易读。这些改进是围绕 CockroachDB 的需求做的,你的负载与它越接近,参考价值越大。
作为 CockroachDB 的存储引擎,Pebble 在 v20.1(2020 年 5 月发布)引入,v20.2(2020 年 11 月发布)成为默认,官方表示已在生产使用且稳定。这段历史说明它能扛住一个分布式数据库的写入压力,也说明它的功能边界是被一个具体产品的需求画出来的。
兼容这条线,比名字暗示的窄
看到”继承 RocksDB 文件格式”,很容易以为可以直接把现有的 RocksDB 数据目录指过去。这一步要非常小心。
Pebble v1 力求与 RocksDB 6.2.1 前向兼容。v2 及更新的版本不再支持直接打开 RocksDB 生成的库,需要先用旧版 Pebble 走 format major version 升级迁移。这个升级是永久的,升级之后不能回退到更早的格式。
即便在兼容的版本上,覆盖范围也只是 CockroachDB 用到的那个功能子集。官方对此有明确警告:如果用 Pebble 打开一个使用了它不支持功能的 RocksDB 数据库,Pebble 可能静默损坏数据,或者行为不正确。静默损坏是这里最刺眼的词,它意味着错误不会在打开时暴露,而是在某次读取时以数据缺失或错乱的形式出现。
还有一条:它不支持 SSTable format version 3 和 4。如果现有库是用这两个版本的格式写出来的,兼容性讨论到这里就结束了。
所以 Pebble 的迁移要按一次正式的数据迁移来做,不能指望换个库名重新打开。合理的做法是先在旧格式上确认功能覆盖,做一次格式升级演练,再用校验工具比对导出前后的数据,最后才切流量。这一段的工作量,通常比改代码大。
LMDB,把整个库映射成一块内存
换个方向看。LMDB 是一个基于 B 树的数据库管理库,API 大致仿照 BerkeleyDB,但简化了很多。它最不一样的地方在于,整个数据库被映射到内存里,读操作直接返回映射内存中的指针,读取过程中没有 malloc,也没有 memcpy,库本身不维护页缓存层。
这件事对读多写少的场景影响很大。从原理上推断,省掉一次从页缓存到用户缓冲区的拷贝,在高频小读的路径上会体现为更低的单次开销;数据在页缓存里命中时,读取基本就是一次指针运算。这是从结构出发的推断,没有对应的基准测试结论,具体到你的数据规模和访问分布,还是建议自己压测。
它提供完整的 ACID 事务。默认是只读映射,应用代码里的野指针写不进库,这一点对 C 与 C++ 混编的项目挺实在;打开 MDB_WRITEMAP 走读写映射,写性能更高,代价是野指针写入可能悄悄破坏数据库。
数据页采用写时复制,活动页不会被覆盖,崩溃之后不需要专门的恢复流程。它不使用预写日志,也不使用 append-only,所以运行期间不需要 checkpoint,也不需要 compaction。空闲页由库自己跟踪并复用,正常使用下文件不会无限增长。对于那些把”运维一个 LSM”当成负担的团队,这一段几乎是理想答案。
一个写者,很多读者,以及几个必须知道的坑
LMDB 的并发模型很简单:写事务完全串行化,同一时刻只有一个写事务,写者之间不会死锁;多版本结构让读者无锁,写者不阻塞读者,读者也不阻塞写者。它是线程安全的,支持多进程多线程并发读写。
代价落在写侧。写入无法并行,单机写吞吐的上限就被这个串行写者钉住了。假设一个团队的负载是大量小写入,比如持续不断的事件流落盘,LSM 那种把写合并成大块顺序 IO 的做法会更合适;反过来,如果负载以读为主,写入只是偶发的元数据更新,串行写根本不是瓶颈,LMDB 的结构反而省掉了 compaction 带来的读抖动和后台 IO。
还有几个官方头文件里写明的注意事项,值得提前知道。残留的过期读事务,比如程序异常退出留下的,会让后续写入使库增长很快,需要定期用 mdb_reader_check 或 mdb_stat 检查。一个线程在同一时间只能用一个事务,子事务算在同一个里。fork 之后不要在新起的子进程里使用 fork 之前打开的 MDB_env。同一个进程内不要同时两次打开同一个 LMDB 数据库。配置往往会预留大量未使用的内存地址空间和可能的文件大小,这不占实际内存或磁盘。通常没有纯只读模式,读者需要锁文件的写权限,只读文件系统或使用 MDB_NOLOCK 是例外。
最后这条在容器化部署里容易踩到:把数据目录挂成只读,读者会起不来。
授权方面,LMDB 用的是 OpenLDAP Public License 2.8,版本日期为 2003 年 8 月 17 日。要点是:源码再分发须保留版权声明;二进制再分发须在文档中复制适用的版权声明、条件列表和免责声明;再分发须包含该许可证原文的逐字副本;不得用作者名义做广告宣传。这是一份偏宽松的许可,义务集中在声明保留上,比 GPL 的传染性条款容易处理。
把三者摆在一起看
前面分开讲的差异,放在一张表里会更清楚。需要说明,表里每一格只写已确认的事实,没有把握的地方留 N/A,避免用印象填空。
| 项目 | RocksDB | Pebble | LMDB |
|---|---|---|---|
| 实现语言 | C++ | Go | C |
| 存储结构 | LSM | LSM,继承 RocksDB 文件格式 | 写时复制 B 树,整库内存映射 |
| 授权 | GPLv2 与 Apache 2.0 双授权,任选其一 | N/A | OpenLDAP Public License 2.8 |
| 是否需要 compaction | 需要,多线程 compaction | 需要,含 level-based compaction 等 | 不需要,也不需要 checkpoint |
| 并发模型 | 多线程 compaction | 提交流水线更快,并发更好 | 写事务串行,读者无锁且不阻塞写者 |
| 与 RocksDB 数据文件兼容 | 本身就是 RocksDB 格式 | v1 力求兼容 RocksDB 6.2.1;v2 起不能直接打开 | 不兼容,数据文件完全不同 |
表里最容易被忽略的一行是”是否需要 compaction”。它看起来是实现细节,其实决定了运维形态:有 compaction 就有后台 IO、有空间放大的波动、有参数要调;没有 compaction 就没有这些,换来的是写侧串行。另一行是兼容性,它决定了迁移是”改个连接串”还是”做一次数据搬迁”。
迁移的账,大半不在代码里
Pebble 的迁移成本集中在两处。功能覆盖要逐项核对,尤其是前面那份未实现清单里的东西;数据文件要走格式升级,而升级是单向的。假设一个团队的库已经用了几年,里面混着不同时期写入的数据,那么在切换之前,最好先在一个副本上把升级、校验、回滚预案都走一遍。回滚预案在格式升级这条路上其实很有限,升级之后回不到旧格式,真正的保险是保留升级前的快照。
LMDB 的迁移成本更直白:数据文件格式完全不同,没有兼容路径,只能把数据导出来再导进去。这中间要有一次全量导出、一次导入、一次逐键比对。数据量越大,这次搬迁就越需要提前规划停机窗口,数据量小则轻得多。导入之后还要重新审视访问模式:原来依赖 LSM 前缀扫描做的范围查询,在 B 树上通常没问题,但如果代码里依赖了 column families 或事务语义,那要改的是设计。
RocksDB 自己也不是没有迁移成本,只是这笔成本已经付过了。团队积累了调参经验,监控指标、告警阈值、故障预案都围绕它建起来。换掉它,这些要重建一遍。这一点在评估时经常被低估。
什么时候该留在 RocksDB
有几种情况,换掉 RocksDB 的收益很难覆盖成本。
数据库规模到了数 TB,写入吞吐要求高,而且写入模式是大量小写。LSM 把随机写转成顺序写的优势在这里最明显,换成 B 树结构要面对另一种形态的写放大。
业务用到了 Pebble 未实现的功能,比如 column families、备份接口、事务。这种情况下 Pebble 直接出局,不需要再比较性能。
已经有成熟的运维体系,团队里有人真正懂 LSM 的调参和故障排查。这时候换库等于把一块已经稳下来的地基重挖。
反过来,如果规模在几十 GB 到几百 GB,读远多于写,团队主语言是 Go 而 C++ 工具链纯粹是负担,那换库的账就容易算平。
怎么选
把三条路放在一起,取舍是有脉络的。
继续用 RocksDB,理由通常是规模和写入压力,以及已有的运维积累。它功能最全,双授权给了合规上的余地,代价是构建复杂度和调参负担。如果眼前的麻烦只是交叉编译,那解决构建通常比换存储便宜得多。
选 Pebble,前提是项目本身是 Go 写的,用到的功能落在它的实现清单内。它继承了 RocksDB 的文件格式和一部分设计,存储结构与 RocksDB 同属 LSM。要接受两件事:功能是子集,数据迁移要走格式升级且不可回退。如果现有库依赖了未实现的功能,或者必须长期和 RocksDB 那套工具链互通,它就不合适。
选 LMDB,前提是读多写少,写入可以串行,数据规模在一块内存映射能覆盖的范围内,团队愿意接受 C 依赖。它把运维面压得很小:没有 compaction,没有 checkpoint,崩溃后不需要恢复流程。要接受的是写侧串行、通常没有纯只读模式,以及那几个关于事务和 fork 的使用约束。
如果一定要给一个顺序:先问数据规模和写入模式,这决定了 LSM 还是 B 树;再问功能覆盖,这决定了 Pebble 是否可行;最后问构建与运维成本,这决定了换库是否值得。三步走完,答案通常只剩一个。
至于性能,这篇里所有关于快慢的说法都是从存储结构推出来的,没有一组数字来自真实压测。真要做决定,拿自己的数据和访问分布跑一轮,比任何对比表都可靠。
参考来源
RocksDB 的仓库见 https://github.com/meta/rocksdb,Pebble 的仓库见 https://github.com/cockroachdb/pebble,LMDB 的只读镜像见 https://github.com/LMDB/lmdb(LMDB 官方仓库在 openldap.org)。上文关于功能清单、兼容边界与授权条款的描述,均取自这三处公开材料。



