ToplingDB CSPP MemTable 设计精要

(一)背景ToplingDB,虽然 fork 自 RocksDB 并且兼容其 API,但实现了脱胎换骨的改进,最重要的就是实现了性能更高的 CSPP MemTable(Rep) 和 SST。CSPP MemTable 基于 Topling CSPP Trie,无论读写,CSPP Trie 的单线程性能都很高,并且多线程性能近乎线性扩展。本文详细介绍如何将 CSPP Trie 的能力应用到 MemTable。在 RocksDB 中,MemTable 设计上分两层,上层叫 MemTable, 下层叫 MemTableRep,上层是 MemTable 公共代码,直接与 DB 进行交互,MemTableRep 是开放的接口层,可以有不同的实现,仅被 MemTable 使用,不直接与 DB 进行交互。(二)CSPP MemTableRepCSPP Trie 的构造函数:MainPatricia::MainPatricia(size_t valsize, intptr_t maxMem,

                       ConcurrentLevel, fstring fpath_or_conf) {...}

其中 valsize 参数指该 CSPP Trie 的所有 Value(此 Value 非 DB 的 Value) 其长度都是定长 valsize,当 valsize == 0 时,CSPP Trie 相当于是个 std::set,非 0 时,相当于 std::map(FixLenValue 必须是 POD 类型)。在 MemTable 中,一个 UserKey 会有多个版本,我们实现该功能对应的逻辑等效品是:std::map > // Tag is uint64{Seq(56 bits), ValueType(8 bits)}
这与 RocksDB 的 SkipList MemTable 不同,SkipList 通过 InternalComparator 来实现该功能,其逻辑等效品是:// InternalComparator 先比较 UserKey,再比较 Tag,Seq 从大到小
std::map, string, InternalComparator>
在 CSPP Trie 的单块内存中,不能用原始指针,而使用 uint32 整数代替指针,并且我们对齐到 4 字节,于是最低 2 bit 都是 0,所以我们将 uint32 转化为 uint64 再左移 2 位,从而就可以用 32 位的整数来实现 16G 的寻址范围。这样做最大的好处是该单块内存可以 memmove,可以存储到文件再直接 mmap……麻烦之处在于,我们要手工进行将偏移转化为内存地址,还要手工实现这种 vector 的追加、搜索等等,一个简易的实现方案就像这样:
ToplingDB CSPP MemTable 设计精要_第1张图片
VecMeta 类似于 std::vector 对象本身(包含 data_ptr, size, capacity),我们同时把 size/num 的最高位作为 lock bit,作为 UserKey 级别的锁——多个线程并发写同一个 UserKey 时。后面的逻辑就都比较简单了:使用 VecMeta 实现逻辑 std::vector,其中 Entry 是:struct alignas(uint32) Entry {
uint32 value_offset;
uint64 tag;
};然后再顺着 Entry.value_offset 访问 Value 内容,Value 内容跟 SkipList 完全相同,都是以变长编码 value length 的作为前缀,后面跟 value 的数据内容。RocksDB 的 MemTable/MemTableRep 原本 Key 和 Value 都是变长 len 前缀编码的,因为其所有的 MemTableRep 都是基于比较的容器,保存了 Key 的原始内容,这样的设计很合理。而 CSPP Trie 并不保存原始 Key,而是在搜索过程中重建 Key,RocksDB 的接口设计就不合适了,所以我们对其进行了重构,将 Key 的类型泛化为 Slice,对其原本的 MemTableRep,转调一下即可。Value 的编码方式我们没有必要改动,仍保持原样,以减少代码修改,并保持兼容性。(三)Copy On WriteCSPP Trie 在多线程并发插入的时候,使用了 Copy On Write,从而前面的 FixLenValue(具体为 VecMeta) 部分就可能会被 Copy On Write,如果线程 A 和 B 并发执行,A 正在 Copy On Write VecMeta,线程 B 正在修改 VecMeta.num,这就完蛋了,线程 B 修改的是旧的 num(线程 A 执行 Copy On Write 之前的 num)!每到这种时候,我们都会想到一个方案:加入一个中间层!于是我们得到:
ToplingDB CSPP MemTable 设计精要_第2张图片
加入这个高亮的中间层,它就是简单的 uint32,作为一个指针,指向 VecMeta,这个 uint32 一旦分配,永不改变,所以任凭 CSPP Trie 怎么 Copy On Write,都不会有影响。首次插入一个 UserKey 时,在 CSPP MemTable 中,牵涉到 3 块内存的分配(在 CSPP Trie 的 mempool 中):len 前缀编码后的 ValueEntryVec 中的 Entry(此时只有一个 Entry)VecMeta这 3 块内存我们最初为每块内存单独调用分配函数,后来意识到,这些内存大多时候会一起访问,特别是大多数 UserKey 都只有一条 Entry。所以这三个内存分配就合成了一次分配,然后三块内存各取所需,所以,逻辑上它们是三块内存,但物理上紧紧相邻,改善了空间局部性,从而提高 CPU cache 命中率。(四)ConvertToSSTCSPP Trie 内部的 mempool 可以是文件 mmap,我们利用这个能力,可以将 MemTable 直接“转化”成 SST,避免了通过 MemTable Flush 遍历 MemTable 将所有 KeyValue 逐条写入 SST。既优化了 IO,又降低了 CPU 和内存的消耗。详细内容,可以看:关于共享内存shm和内存映射mmap的区别是什么? - 知乎 (zhihu.com)
ToplingDB CSPP MemTable 设计精要_第3张图片
【完】

你可能感兴趣的:(后端数据库mysql)