- 分享人:李林峰
- 关键词:SSD,LSM-tree,KV Separate,compaction
- 分享PPT: 基于SSD的LSM-tree键值分离
-
通常情况下,数据库存储引擎处理更新采用两种方法:就地更新(in-place updates)和异地更新(out-of-place updates)。就地更新结构(比如B+树)是直接将新的数据覆盖到原有的位置,这样虽然会带来好的查询性能,但是这样做导致随机IO,会极大降低写性能,并且多次更新和删除会严重导致磁盘页面碎片化问题,从而降低了空间利用率。
-
相反,异地更新结构(比如LSM树)则是将更新数据存储在新的位置,而不是覆盖原有的旧数据。这样做利用了顺序IO,从而提高了写性能。但是这样做牺牲了读性能。
LSM-tree被广泛应用,一个重要的原因就是因为在以前,机械硬盘应用广泛,这种硬盘他的顺序写性能远远好于其随机写性能,所以为了提升系统的一个整体的吞吐量,所以选用LSM Tree ,采用异地更新来实现快速写入。但LSM-tree存在严重的读写放大
-
SSD随机和顺序性能之间的差异远没有硬盘那么大,执行大量顺序I/O以减少后来的随机I/O可能会不必要地浪费带宽
-
SSD具有很大程度的内部并行性
-
SSD可能会通过重复写入而磨损,LSM tree中的高写入放大会显著降低设备寿命
-
SSD特有的写入擦除循环和昂贵的垃圾回收,过量的随机写有害
-
SSD是由许多闪存颗粒组成,又细分为多个Block,每个Block又包含多个Page。由于固有的特征,它的最小读写单位是Page、
-
同时已经有数据的page不能直接覆盖,必须将它进行擦除操作才能写入,但是由于擦除机制的影响,是以Block为最小单位。
-
对于随机写入,因为写入的单位是page,如果你随机写入的数据很小,也依然要占用一个page,这样就有了好几倍的写放大,降低性能的同时会损耗SSD的宝贵寿命。
-
GC过程,他会将其中的数据迁移到空白块中,之后再擦除这两个物理块。整个垃圾回收写入和读取了大约两倍Block的数据量。频繁的随机I/O会创造很多无效的Page,而随着Block中无效Page的增加,就会垃圾回收,而垃圾回收的I/O放大很严重,对SSD的性能影响也很大。
-
Key 放在 LSM 树里面,Value 放在分离开的 Value log 中
-
compaction 时,只会对 Key 进行排序和重写,不影响Value log,显著降低了写放大
-
Value 分离后,LSM-Tree 本身大幅减小,可以减少查询时从磁盘读取的次数,并且可以更好的利用cache
-
Value log需要定期进行GC,开销较大
2.1 wisckey
-
value放在value-log 文件中
-
不写WAL,以 <vLog-offset, value-size> 形式插入 LSM 树中
-
Compaction:只需将无效key从 LSM 树中移除,不需要修改vLog
-
范围查询:利用内部线程池并行读取 value 数据。
-
Value log:循环队列,tail至head之间部分包含所有value
-
GC:依次读取一个键值对块,返回 LSM 中检查value有效性,将有效值写回vLog 的head
-
GC完成后将新的 value 地址和 tail 指针地址写回到 LSM-Tree 中
缺点:
-
严格的 gc 顺序,从 head -> tail,造成很多不必要的写放大
-
GC时通过LSM-tree 判断value 有效性,查询开销不容忽视。
-
GC完成后还需要将最新的value指针更新到LSM-tree,导致大量不必要的数据重定位
-
对于小value的范围查询性能较差
2.2 HashKV
部分KV分离:
-
小value放在LSM中,大value写入value log
-
小value对整个系统的平均写放大都影响很小
-
改善它的一个查询性能
-
减小GC开销
减小写放大方法:
-
对key进行hash将value放在不同partition
-
对value进行冷热分离,最后一次插入以来至少更新过一次的KV对视为热数据
Garbage Collection
-
以 segment group 为单位,选择写入量最大的segment
-
检查KV有效性:不需要查找 LSM-tree
-
构造临时内存哈希表(按键索引)来缓冲在段组中找到的有效KV对的地址
-
Cold data log进行GC,与wisckey一致
-
更新 LSM-tree中的地址信息
2.3 TerarkDB
-
value 需要在前台被写入 WAL
-
部分KV分离:大 value写入 v-SST 中
-
V-SST 按 key 排序,LSM tree仅存储<key, fileno>
GC:
-
GC 无需更新LSM tree,在 MANIFEST 中记录 v-SST 之间的依赖关系,降低了 GC 对用户前台流量的影响
-
compaction 过程中顺便更新value 位置信息
2.4 TItan
-
部分KV分离,存储形式 :<key, <fileno, offset>>
-
Blob file中的value有序
传统GC
-
使用BlobFileSizeProperties和 discardable size来收集 GC 所需信息
-
每个BlobFile在内存中维护一个 discardable size 变量
-
需要去LSM-tree查询value有效性,GC完成后需要写回SST
Level Merge
-
Compaction的同时进行GC
-
省去了有效性检查和更新SSTable
-
造成写放大,仅对 LSM-tree 中 Compaction 到最后两层数据对应的 BlobFile 进行 Level Merge
2.5 NovKV
设计亮点:
-
Vstore分为Vtable和Svtable,新写入的数据写入Vtable
-
GC的时候,利用DropKeys来检查kV的有效性
-
KV被读取的时候再更新GC后的epoch,file offset,item size
-
VTable:顺序写入
SVTable:便于查询
-
在key的尾部添加一个sequence number
-
Compaction:创建一个map
map key :file number;
map Value : DropKeys组成的vector
GC流程:
-
选择valid rate最小的VTable或者SVTable进行GC
-
从后往前读取Drop keys,并将其全部插入hash set
-
遍历KV Iterm,与hash set做对比,将valid value写入新的SVTable(memtable)
-
将memtable刷盘(SSTable),并在末尾添加一个invalid Dfooter
-
GC后的结果当这个KV被读取的时再更新(通过epoch判断)
2.6 UniKV
优化点:采用分层体系结构实现差异化数据索引
保证 read/write/scan/Scalability 的高效
-
UnsortedStore:内存刷回的数据,无序SortedStore:Unsorted Store合并后的有序的数据
-
内存 HASH 索引来支持快速的读写
-
SortedStore采用KV分离
Hash Indexing
-
轻量级的两级 HASH:cuckoo hashing+linked hashing
-
索引项:<keyTag, SSTableID, pointer>
-
写入:从h_1一直到h_n,直到找到空的bucket,否则生成一个overflow entry添加到对应的桶 h_n (key)%N 后
读取:从h_n一直到h_1
KV分离
-
元信息:<partition,logNumber,offset,length>
-
动态范围分区:根据Key大小进行拆分,改善范围查询
-
GC:需要遍历Sortedstore,判断有效性。之后还需要将元信息写回sortedstore、
KV分离改进措施:
-
部分KV分离,在不影响系统整体写放大的情况下,减少GC开销,提高范围查询性能(HashKV)
-
对key进行hash、或按key的大小将value放在不同partition,减小写放大(HashKV,UniKV)
-
冷热数据分离,减小冷数据GC造成的写放大(HashKV)
-
通过 MANIFEST 记录GC前后文件之间的依赖关系,推迟至compaction 过程中更新value 位置信息(TerarkDB)
-
在最后1~2 level,Compaction的同时进行GC,省去了有效性检查和更新SSTable(Titan)
-
Compaction时将Dropkey写入VStore,GC时利用DropKeys来检查kV的有效性(NovKV)
-
KV被读取时再更新SSTable(NovKV)