(一)研究目标
结合B+Tree利用PM对LSM-Tree进行优化。
(二)研究背景
LevelDB 存在的问题:较低的读性能和严重的读写放大。
Slow read operations:
一个读操作至少需要两次块读取,一次读索引块,另一次读数据块。
先搜索内存中的MemTable和Imutable Table,找不到则在磁盘上的每一个 Level 上根据SSTable的开始key进行二分查找,定位数据所在的SSTable,然后对 SSTable File 中存储的数据索引再次进行二分查找,找到对应的数据块。为了避免不必要的块读取和减少搜索成本,LevelDB对每个块使用Bloom过滤器。
High write and read amplification
写放大:
- 维护垂直的多级有序数据结构 SSTables 来保证顺序写
- 需要在不同的层级之间进行归并排序操作,同时需要常常进行数据压缩(Compaction)
读放大: - 由于 LSM 树的原因,读数据时往往需要在多个层级进行检索
- 不仅需要读取该 Key 所对应的数据所在的数据块,还要读取对应的索引块和 Bloom Filter 块,索引块和 Bloom Filter块的数据和可能比 KV 本身大得多
(三)研究概述
使用 PM 进行缓冲写入,省略了 WAL 的写开销,采用单层的 LSM 结构来减小写放大问题带来的影响。利用 PM 的特性维护 B+ Tree 作为索引,加速数据的检索。
(四)关键技术
Persistent MemTable
MemTable 的核心数据结构为跳表(SkipList)。跳表的操作,如插入、更新、删除均可用 8bytes 的原子操作完成。采用PM存储MemTable, SLM-DB就不需要依赖于W AL来保证数据一致性。跳表的索引部分不需要提供一致性保证,因为可以从原始数据中快速重建。
插入操作:为了保证MemTable中kv的一致性,首先调用内存fence和cacheline flush指令持久化一个新节点,该节点的next指针被设置指向。然后更新其上一个节点的next指针(8字节)指向新节点,并持久化更改。
Insert(key, value, prevNode)
1: curNode := NewNode(key, value); // Create Node
2: curNode.next := prevNode.next; // Modify currentNode.next pointer
3: mfence(); // 插入内存屏障(读屏障),让高速缓存中的数据失效,重新从主内存加载数据
4: clflush(curNode); // 缓存刷新, 持久化 currentNode 数据
5: mfence(); // 插入内存屏障(写屏障),内存隔离,让写入缓存的最新数据写回到主内存
6: prevNode.next := curNode; // Modify preNode.next pointer
7: mfence(); // 插入内存屏障(读屏障),让高速缓存中的数据失效,重新从主内存加载数据
8: clflush(prevNode.next); // 缓存刷新,持久化 preNode 的修改
9: mfence(); // 插入内存屏障(写屏障),内存隔离,让写入缓存的最新数据写回到主内存
B+tree Index in PM
基本操作:
- 当将Imutable MemTable中的KV对刷新到SSTable时,key相关的索引信息被插入到B+树的叶节点,其指针指向一个PM对象,该对象包含键值对在磁盘上的位置信息、SSTable file ID、file内键值对的块偏移量、块的大小。
- B+ Tree 的更新操作:更新已有的 KV 对时,首先创建对应该 Key 的索引对象,然后修改原有的在 B+ 树中的指针,指向新的索引对象。旧的索引信息将会被视为垃圾数据进行统一的垃圾回收。
- B+ Tree 的删除操作:当需要删除指定 Key 的数据时,则将该 Key 在 B+ 树中对应的索引信息删除。
- 垃圾回收时使用 PM 管理器如 PMDK 来进行垃圾回收。
对于刷新操作,SLM-DB会创建两个后台线程,一个用于创建文件,另一个用于插入B+tree。
- file creation thread:在文件创建线程中,SLM-DB创建一个新的SSTable文件,并将KV对从Immutable MemTable写入该文件。一旦文件创建线程将文件刷新到磁盘,它就将存储在新创建的文件上的所有KV对添加到一个队列中,这个队列是由B+tree插入线程创建的。
- B+tree insertion thread:创建相应的 KV 索引信息队列,待数据写入线程将该队列填入数据之后,依次处理队列中的每个数据,构建 B+ 树。
- 一致性保证:待 B+ Tree构建完成以后,将 LSM 树中组织结构的变化(例如 SSTable File 的元数据信息)以追加写的方式写入到 MANIFEST 文件中。
- 待完成一致性的日志追加写操作之后,对应的删除PM中的 Immutable Memtable
Scanning a B+tree
- SLM-DB 提供了一个迭代器,主要用于扫描存储在磁盘中的 KV 键值对。支持 seek()、value()、next()等操作。
- seek(k):让迭代器指向 key 为 k 的键值对或者当 k 不存在时,指向比 k 稍大的 Key
- next():将迭代器指针移动到下一个 Key
- value():返回该 Key 对应的值
Selective Compaction
SLM-DB 支持选择性的压缩操作,目的:
- 回收废弃的KV
- 提高KV在SSTable的顺序性
SLM-DB 维护一个压缩候选 SSTable 列表。当执行一个压缩线程时,SLM-DB 从候选列表中根据key的重叠率选择SSTable的一个子集进行压缩合并。
压缩过程也使用文件创建线程和B+Tree索引插入线程。合并多个 SSTable Files 时,需要检查该文件中的每一个键值对是否有效或过时。有效的数据进行归并排序,老旧的数据则对应将其删除。压缩过程中,文件创建线程创建新的SSTable文件(存压缩合并的文件),然后刷新到磁盘,然后将新文件中包含的所有KV对添加到B+Tree插入线程。然后文件创建线程创建新文件,插入线程更新KV对,重复这个过程直到完成压缩合并。最后将SSTable元数据的更改提交到MANIFEST文件,并删除过时的SSTable文件。
为了选择用于压缩的候选SSTable,采用三种选择算法:
- 基于SSTable的有效Key进行选择(目的是回收磁盘上的垃圾):统计每个SSTable的有效Key的比例,一旦比例低于设定的阈值,就被添加到候选压缩列表中。(有效Key指维护在B+Tree中的Key)
- 基于B+Tree的叶节点扫描进行选择(目的是提高存储在L0中KV的顺序性):扫描叶节点,统计包含指定Key的SSTable文件的数量,如果数量大于阈值,则将这些文件添加到候选列表。
- 基于范围查询的有序程度进行选择(提高数据分布的顺序性):将范围查询查到的键划分为若干子范围,统计每个子范围中SSTable的数量,找到拥有最大SSTable数量的子范围,若数量大于阈值,则将这些SSTable添加到候选列表。
(五)实验
实验配置:
- two Intel Xeon Octa-core E5-2640V3 processors (2.6Ghz)
- Intel SSD DC S3520 of 480GB
- disable one of the sockets and its memory module and only use the remaining socket composed of 8 cores with 16GB DRAM
- Ubuntu 18.04 LTS with Linux kernel version 4.15
- emulate PM using DRAM
评估了SLM-DB的性能,并将其与LevelDB(版本1.20)在不同的值大小下的性能进行了比较。
- MemTable size:64MB
- a fixed key size:20bytes
三种选择算法的阈值:
- live-key threshold = 0.7
- leaf node threshold = 10
- sequentiality degree threshold = 8 (子范围有30个key)
使用 db_bench benchmarks 作为微基准测试,使用YCSB作为真实世界的工作负载基准测试。
与LevelDB相比,SLM-DB的写时延和总写数据量平均减少了49%和57%。这是因为SLM-DB通过在单个Level组织sstable并执行限制性压缩进一步减少了写放大。
- 对于随机写操作,各value大小下,SLM-DB的平均吞吐量大约是LevelDB的2倍。这是通过显著减少写入磁盘进行压缩的数据量来实现的。对B+树的插入是由后台线程执行的,开销很小。
- 对于随机读操作,根据值的大小,SLM-DB表现出与LevelDB相似或更好的性能。随着数值大小的增加,SLM-DB和LevelDB之间的性能差异增大,这是由于SLM-DB中使用B+tree索引对KV对进行高效搜索。但是,当值大小达到64KB时,从磁盘读取数据块所花费的时间会比读取较小值的时间长。因此,SLM-DB和LevelDB之间的性能差异下降到25%。
- 对于小范围查询操作,每级KV全顺序的LevelDB可以连续读取给定范围内的KV对,对于1KB和4KB大小的值具有更好的性能。
- 对于顺序读工作负载扫描所有KV对,SLM-DB的性能优于LevelDB,除了1KB的值大小。