前言
目前笔者本人正在基于 Pulsar 搭建公司内部的消息平台,自然也对其底层存储做了一些研究。Pulsar 使用 BookKeeper 作为存储层,BookKeeper 底层使用到了 RocksDB 来保存 Entry (BookKeeper 中的数据存储单元) 对应的位置索引。RocksDB 是我一直关注的存储引擎技术,因为之前在调研持久型 KV 存储的时候,发现主流开源的 pika/kvrocks,以及最终选用的云厂商的持久型 KV 存储服务,底层都是基于 RocksDB。还有大名鼎鼎的 TiDB,其存储引擎也是 RocksDB。
怀着好奇,我开始了对于 RocksDB 的学习,由于 RocksDB 一般用于底层开发,如果不是开发数据存储中间件,日常很难接触到,所以我决定先去学习 RocksDB 的数据结构设计:LSM 树。
本文先是介绍了 RocksDB 对于LSM 树的实现,再总结了 LSM 树的设计思想,也类比了 Elasticsearch Lucene 的存储设计思想,最后将 LSM 树和常见的 B+ 树做了对比。
LSM 树 简介
LSM 树,全称 Log-Structured-Merge Tree。初看名字你可能认为它会是一个树,但其实不是,LSM 树实际上是一个复杂的算法设计。这个算法设计源自 Google 的 Bigtable 论文 (引入了术语 SSTable 和 memtable )。
基于 LSM 树算法设计实现的存储引擎,我们称之为 LSM 存储引擎。在 LevelDB、RocksDB、Cassandra、HBase 都基于 LSM 树算法实现了对应的存储引擎。
下面我们通过 RocksDB 的 LSM 树实现,来详细了解一下 LSM 树的设计思想。如果只想看 LSM 树的设计思想总结,可以跳转到最后的总结部分,私以为总结的还是不错的。
RocksDB LSM 树 实现
1. 核心组成
首先,先看看 RocksDB 的三种基本文件格式 memtable & WAL & SSTable。
下图描述了 RocksDB LSM 树的核心组成和关键流程步骤(读 & 写 & flush & compaction)。
1.1 memtable (active & immutable)
memtable 是 RocksDB 内存中的数据结构,同时服务于读和写;数据在写入时总会将数据写入 active memtable,执行查询的时候总是要先查询 memtable,因为 memtable 中的数据总是更新的;memtable 实现方式是 skiplist,适合范围查询和插入;
memtable 生命周期
当一个 active memtable 被写满,会被置为只读状态,变成 immutable memtable。然后会创建一块新的 active memtable 来提供写入。
immutable memtable 会在内存中保留,等待后台线程对其进行 flush 操作。flush 的触发条件是 immutable memtable 数量超过 min_write_buffer_number_to_merge 。flush 会一次把 immutable memtable 合并压缩后写入磁盘的 L0 sst 中。 flush 之后对应的 memtable 会被销毁。
相关参数:
write_buffer_size:一块 memtable 的 容量
max_write_buffer_number:memtable 的最大存在数
min_write_buffer_number_to_merge:设置刷入 sst 之前,最小可合并的 memtable 数(如果设置成 1,代表着 flush 过程中没有合并压缩操作)
1.2 WAL (write-ahead log)
WAL 大家应该都很熟悉,一种有利于顺序写的持久化日志文件。很多存储系统中都有类似的设计(如 MySQL 的 redo log/undo log、ZK 的 WAL);
RocksDB 每次写数据都会先写入 WAL 再写入 memtable,在发生故障时,通过重放 WAL 恢复内存中的数据,保证数据一致性。
这种设计有什么好处呢?这样 LSM 树就可以将有易失性(volatile)的内存看做是持久型存储,并且信任内存上的数据。
至于 WAL 的创建删除时机,每次 flush 一个 CF(列族数据,下文会提) 后,都会新建一个 WAL。这并不意味着旧的 WAL 会被删除,因为别的 CF 数据可能还没有落盘,只有所有的 CF 数据都被 flush 且所有的 WAL 有关的