Titan 存储引擎的设计

Titan 是 pingcap 开源的一个基于 RocksDB 的 存储引擎,以插件的形式提供,通过 key value 分离降低在 compaction 过程中的写放大。整个项目是受 WiscKey 的启发。本文主要讨论 Titan 在 RocksDB 基础之上写入和查询机制,基于 Titan 代码版本为 54a20a5@master。

Titan 架构图

在 LSM-Tree compaction 过程中,只需要对 key 进行排序。通常情况下,key 大小远小于 value。因此,将 key value 分别进行存储,在 compaction 过程中只对 key 文件进行排序,可有效减小 compaction 过程带来的写放大。
WiscKey 深入贯彻了这个思想,所有的 value 都存储在单独的 vLog (value log) 文件中,LSM-tree 中则存储 key 对应 value 在 vLog 中的地址。当一个键值对写入到 WiscKey 时,value 追加写入到 vLog 中,key 和 value 对应的 vLog 的地址则写入到 LSM tree 中。以 16B/1KB 大小的 kv 为例,对于一个写放大为 10 的 LSM-tree,WiscKey 最终的写放大是 (10 * 16 + 1024) / (16 + 1024) = 1.14。
Titan 在 RocksDB 基础上,引入 blob file 作为较大 value 的存储介质,原有的 LSM-tree 存储访问 blob file 中 value 的handler。同时,为 blob file 引入垃圾回收机制,以支持更新/删除等操作带来的 blob file 数据冗余。

Blob File

与 RocksDB 一样,Titan 在进行数据写入时依然是先写入 WAL 日志,然后写入到 memtable 中。在一定条件下,memtable 是可以支持多线程并发写入的。在 memtable 执行 flush 操作过程中,与 RocksDB 不同的是,Titan 会挑选大小超过 4KB 的 value 通过 BlobFileBuilder 生成 blob file 中以实现 kv 分离。

Blob FIle Layout

blob file 的 layout 如图所示,包括定长的 header、若干 blob record、若干 meta block 和 1 个 meta block index,以及定长的 footer。


blob file layout
  • header 保存了 magic number 和 version 字段,以及一个目前只在 version 2 才有的 flags 字段,flags 目前保存的是解压字典标志位;
  • record 则是实际的用户数据,是一个变长的结构。blob file 中的 record 在写入时按照 key 顺序进行存储,以提高顺序读取能力;
  • meta block 属于可选项,目前只有存储解压字典的类型。相应的 meta block index 则存储 meta block 的索引信息;
  • footer 中存储 meta index block 的索引信息。

可以看到,由于 blob file 本身无需直接支持检索,blob file 中并未存储访问 record 的索引信息。blob file 的访问是通过存储在 LSM-tree 中的 blob index 来完成,blob index 包括 record 所在的 blob file 文件名、文件偏移量和数据大小。record 中额外存储了相应的 key 信息,是为了通过 record 中的 key 可以快速查询到 LSM-tree 中存储的相应 blob index,从而便于通过比对 blob index 和 record 的内容进行 GC 等操作。

数据写入

BlobFileBuilder

在 flush 过程中,Titan 进行 blob file 的构建。flush 是指将 LSM-tree 的 memtable 数据刷入到 Level 0 的 SST 文件的过程,可以由用户调用 DB::Flush() 手动触发,也可以由于 WAL 或 memtable 的大小超过阈值触发。利用 RocksDB 高度扩展性,Titan 实现了 rocksdb::TableBuilder 虚基类,通过重载其工厂函数实现自定义 rocksdb::titandb::TitanTableBuilder 的注入。Titan 在 flush 过程中,选取 value.size() 超过阈值的 value 以 blob record 的形式写入到 blob file 中,该阈值可以通过
TitanCFOptions.min_blob_size 进行设置,默认为4096。同时,Titan 会在 LSM-tree 的对应 value 中记录 record 的访问 blob index,以实现对 blob file 中数据的查询。对于大小小于阈值的 value,退化成普通的 RocksDB flush 流程,直接存储 key 和 value。

数据查询

数据结构

UML类图
  • BlobFileSet 是管理和访问 blob file 的入口,通过 BlobStorage 以 column family 为单位进行管理;
  • BlobStorage 管理一个 column family 对应的 blob 文件,包括打开的文件的元信息以及打开的 blob file 的文件 BlobFileCache;
  • BlobFileCache 通过 file cache 管理打开的 blob file 文件,BlobFileCache 中的 file cache 指针与 BlobFileSet 中的 file cache 指针指向相同的对象。cache 的 key 是 file number,这是一个在构建 blob file 时生成的唯一序号。cache 的 value 则是用于读取 blob 文件的 BlobFileReader。在查询 cache 时,如果发现 cache 中找不到需要的 key,则构建一个对应文件的 BlobFileReader;
  • BlobFileReader 执行实际的 blob file 读取操作。在创建 Titan 实例时配置 blob cache,可发挥与 RocksDB 的 block cache 类似的作用。cache 的 key 由 blob 文件信息和 handle offset 两部分编码而成,后者对应 blob record 在文件中存储的偏移量。

执行流程

数据查询的执行流程

数据读取先查询 LSM-tree 中的 key 对应的 value,对于 blob index 类型的 value,根据 blob index 的索引信息检索 blob file 中的 blob record 进而得到返回的数据;对于普通的 value,则可直接返回。

点查询

点查询在此处是指调用 Get() 接口,一次获取一个 key 对应的 value 的查询。 尽管如此,由于 LSM-tree 中只存储较小的键值对,在同等内存容量下的 Titan 可以缓存更多的索引、布隆过滤器和 block。尽管在 RocksDB 的读取流程基础上,Titan 对于 blob record 额外增加了一次 blob file 的读取。但由于更优的缓存,Titan 大部分点查询 和 RocksDB 的查询一样,只需要一次 IO。不仅如此,由于更低的 LSM-tree 层级以及更小的数据 block,Titan 拥有更小的读放大,点查询性能相对于 RocksDB 有显著提升。

范围查询

为了提升范围查询能力,blob record 按照 key 字典顺序进行存储。同时,充分利用操作系统的 page cache 机制,通过 readahead() 系统调用将文件内容提前缓存到操作系统中,避免执行实际操作时才进行 IO 造成较大延时。尽管如此,Titan 的范围查询性能依然显著低于 RocksDB。需要指出的是,Wisckey 论文中有提到利用 SSD 的并行特性来优化范围查询性能,通过线程池并行预取数据到内存中,在 value 较大的场景下可以得到近似于 LevelDB 的性能。

参考文献

WiscKey: Separating Keys from Values in SSD-Conscious Storage
Titan Overview
RocksDB Wiki

你可能感兴趣的:(Titan 存储引擎的设计)