跳转至

MiniOB LSM-Tree 存储引擎

LSM-Tree 背景介绍

LSM-Tree 是一种数据结构,可用于存储键值对。LSM-Tree 采用了多层的结构,存储部分可以分为内存和磁盘两个部分。内存中的部分称为 MemTable,磁盘中的部分称为 SSTable(Sorted String Table)。LSM 树通过 Append-Only 的方式提供高效的数据写入,为了优化读取性能,LSM-Tree 通过 Compaction 操作定期重新组织数据。

OceanBase 数据库的存储引擎也是基于 LSM-Tree 架构,将数据分为静态基线数据(放在 SSTable 中)和动态增量数据(放在 MemTable 中)两部分,其中 SSTable 是只读的,一旦生成就不再被修改,存储于磁盘;MemTable 支持读写,存储于内存。数据库 DML 操作插入、更新、删除等首先写入 MemTable,等到 MemTable 达到一定大小时转储到磁盘成为 SSTable。在进行查询时,需要分别对 SSTable 和 MemTable 进行查询,并将查询结果进行归并,返回给 SQL 层归并后的查询结果。同时在内存实现了 Block Cache 和 Row cache,来避免对基线数据的随机读。关于 OceanBase 数据库的更多细节可以参考:https://www.oceanbase.com/docs/oceanbase-database-cn

ObLsm 是一个为教学设计的 LSM-Tree 结构的 Key-Value 存储引擎。ObLsm 本身是一个独立于 MiniOB 的模块,可以独立编译使用。ObLsm 包含了 LSM-Tree 中的关键结构,可以帮助大家学习 LSM-Tree 架构。 MiniOB 中也基于 ObLsm 实现了一个基于 LSM-Tree 的表引擎,可以将表数据以 Key-Value 的格式存储到磁盘。

ObLsm 存储引擎

下面会对 ObLsm 的各个模块作简单介绍,便于大家对 ObLsm 有一个初步的了解,更多细节可以参考源代码 src/oblsm

MemTable

MemTable 是一种内存数据结构,用作处理即将到来的操作(insert/delete/update)的缓冲区(buffer)。很多数据结构都可以用于 MemTable 的实现,现有的 LSM-Tree 实现(如 LevelDB/RocksDB)中多采用 SkipList,ObLsm 目前也使用 SkipList 作为 MemTable 的底层数据结构。ObLsm 将 insert/update/delete 都视作一条记录来插入到 MemTable 中。

  • insert:将一条记录插入到 MemTable 中。
  • update:将一条时间戳更大的记录插入到 MemTable 中。
  • delete:将一条 value 为空的记录插入到 MemTable 中。

MemTable 将插入的 Key-Value 编码为如下的记录存储。

    ┌───────────┬──────────────┬───────┬──────────────┬──────────────────┐
    │           │              │       │              │                  │
    │key_size(8)│ key(key_size)│ seq(8)│ value_size(8)│ value(value_size)│
    │           │              │       │              │                  │
    └───────────┴──────────────┴───────┴──────────────┴──────────────────┘

其中,key_size 和 value_size 分别表示 key+seq 和 value 的长度,seq 表示记录的时间戳。括号中表示占用字节数。

MemTable 的实现位于:src/oblsm/memtable/,在代码中,我们将上图中的key 称为 user_key,将 key + seq 称为 internal_key,将key_size + key + seq 称为 lookup_key。

SkipList

SkipList(跳表)是用于有序元素序列快速搜索的一个数据结构,SkipList 是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。SkipList 在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,实现也比红黑树简单很多。

SkipList 的实现位于:src/oblsm/memtable/ob_skiplist.h

MemTableIterator

MemTableIterator 提供了一种遍历 MemTable 的机制。它可以按序访问 MemTable 中的所有键值对。

MemTableIterator 的实现位于:src/oblsm/memtable/ob_memtable.h::ObMemTableIterator

MemTable 转储

MemTable 转储是将内存中的 MemTable 持久化到磁盘上的过程。当 MemTable 达到一定大小时,会被转储为不可变的 SSTable。转储过程通常包括排序数据、生成 SSTable 文件并将其写入磁盘。

转储相关代码位于:src/oblsm/oblsm_impl.h::ObLsmImpl::try_freeze_memtable

SSTable

MemTable 的大小达到限制条件,MemTable 的数据以按顺序被转储到磁盘中,被转储到磁盘中的结构称为(SSTable:Sorted Strings table)。

SSTable 是一种有序的键值对存储结构,它通常包含一个或多个块(block),每个块中包含一组有序的键值对。

SSTable 的存储格式示例如下:

      ┌─────────────────┐
      │    block 1      │◄───┐
      ├─────────────────┤    │
      │    block 2      │    │
      ├─────────────────┤    │
      │      ..         │    │
      ├─────────────────┤    │
      │    block n      │◄─┐ │
      ├─────────────────┤  │ │
 ┌───►│  meta size(n)   │  │ │
 │    ├─────────────────┤  │ │
 │    │  block meta 1   ├──┼─┘
 │    ├─────────────────┤  │
 │    │      ..         │  │
 │    ├─────────────────┤  │
 │    │  block meta n   ├──┘
 │    ├─────────────────┤
 └────┤                 │
      └─────────────────┘

其中,block 表示由若干键值对组成的数据块。block meta 用于存储 block 的元信息,包括 block 的大小、block 的位置信息,block 中的键值对数量等。

SSTable 的实现位于:src/oblsm/table/

Block

为了提高整体的读写效率,一个sstable文件按照固定大小划分为 Block。每个Block中,目前只存储了键值对数据。 Block 的存储格式如下:

      ┌─────────────────┐
      │    entry 1      │◄───┐
      ├─────────────────┤    │
      │    entry 2      │    │
      ├─────────────────┤    │
      │      ..         │    │
      ├─────────────────┤    │
      │    entry n      │◄─┐ │
      ├─────────────────┤  │ │
 ┌───►│  offset size(n) │  │ │
 │    ├─────────────────┤  │ │
 │    │    offset 1     ├──┼─┘
 │    ├─────────────────┤  │
 │    │      ..         │  │
 │    ├─────────────────┤  │
 │    │    offset n     ├──┘
 │    ├─────────────────┤
 └────┤  offset start   │
      └─────────────────┘

Block 的主要实现位于 src/oblsm/table/ob_block.h

SSTableBuilder

用于构造一个 SSTable,主要实现位于 src/oblsm/table/ob_sstable_builder.h

BlockBuilder

用于构造一个 Block,主要实现位于 src/oblsm/table/ob_block_builder.h

Compaction

Compaction 是 LSM-Tree 的关键组件,Compaction 会将多个 SSTable 合并为一个或多个新的 SSTable。Compaction 的实现主要位于 src/oblsm/compaction/

基于 ObLsm 的表引擎

MiniOB 基于 ObLsm 模块实现了一个 LSM-Tree 表引擎,用于以 Key-Value 格式存储表数据。表引擎的实现位于:src/observer/storage/table/lsm_table_engine.h

LSM-Tree 表引擎使用方法: 在创建表时指定 engine=lsm,即可使用 LSM-Tree 表引擎。 当不指定engine 或指定 engine=heap,将使用堆表作为表数据的存储方式。

create table t1 (id int primary key, name varchar(20))engine=lsm;

在 MiniOB 中,使用关系型模型来描述表结构,而 LSM-Tree 表引擎将表数据以 Key-Value 的形式存储到磁盘,因此需要提供一种机制来将关系型模型转换为 Key-Value 模型。 目前 MiniOB 中以自增列作为 Key,将行数据以 Table::make_record 编码为 Value。通过 orderedcode 来对 Key 列做编码,使得在编码后 Key 的字典序上比较与 Key 对应的原始序列(目前可以认为只有自增列一列,后续支持主键后,Key 会对应表中的多列)上进行比较具有相同的顺序。

此外,为了在同一个 LSM-Tree 引擎中存储多张表的数据,MiniOB 为每一张表分配一个 TableID,并在 Key 中加入 TableID 作为前缀。

因此,每行数据按照如下规则编码成 (Key, Value) 键值对:

Key:   t{TableID}r{AutoIncID}
Value: [col1, col2, col3, col4]

参考资料

  1. OceanBase
  2. LSM-Tree
  3. LevelDB
  4. RocksDB
  5. Mini-LSM