【论文阅读】WiscKey
WiscKey介绍
WiscKey是一篇发表于存储领域顶尖学术会议 FAST '16 的里程碑式论文(WiscKey: Separating Keys from Values in SSD-Conscious Key-Value Stores),由威斯康星大学麦迪逊分校(UW-Madison)的 ADSL 实验室提出。
LSM-Tree的局限
LSM-Tree 诞生于HDD时代,HDD 的随机读写比顺序读写慢 100 倍以上,因此用多次顺序读写来换取后续高效查询的代价是划算的。这会带来IO放大
现代 SSD 具有随机与顺序性能差距小、内部并行度高以及有擦写寿命限制的特点。继续采用传统 LSM-Tree 架构会导致 SSD 吞吐量下降高达 90%,无法释放SSD的真正性能。
WiscKey
WiscKey将 Key 和 Value 分离存储,Key 依然在 LSM-Tree 中排序管理,而 Value 提取出来单独保存在日志文件中。通过避免排序过程中不必要的值移动,显著降低写放大效应;同时 LSM 树的规模明显减小,从而减少磁盘读取次数,提升查询时的缓存效率。
键值分离后带来了新的难题,WiscKey 提出了对应的优化方案:
- 范围查询变慢:因为 Value 不再按 Key 的顺序物理存储。
- 解法:充分利用现代 SSD 强大的内部并发读取能力来提升 Scan 性能。
- 空间回收:无效/旧版本的 Value 需要被清理。
- 解法:设计轻量级的在线 GC 机制,全过程仅包含顺序 I/O,对前台读写性能影响极小。
- 崩溃一致性:分离存储后如何保证crash时数据一致性保证。
- 解法:利用现代文件系统追加写入在crash的时候不会产生垃圾的特性,提供与 LevelDB 完全相同的强一致性保证。
性能提升:
- LevelDB的Microbenchmarks:
加载数据库:比 LevelDB 快 2.5× – 111×
随机查询:比LevelDB块1.6×–14×
随机写入小 Value和大型数据集顺序scan:比LevelDB性能差 - YCSB
WiscKey在全部六种 YCSB 工作负载中均优于LevelDB和RocksDB
读放大和写放大
写(读)放大率定义为写入(从)底层存储设备的数据量与用户请求的数据量之比。
写放大
写放大来源:在 Compaction过程中需要读取一个LiL_iLi的SSTable和最多10个Li+1L_{i+1}Li+1的SSTable。
数据每跨越一层,写放大最高可达 10 倍。对于有很多数据的db,新生成的SSTable最终都会通过一系列 Compaction 从L0L_0L0一步步迁移至L6L_6L6。在这个过程中,写放大逐层累加,总写放大可以超过 50 倍。
在图2中,1 GB数据量下写放大为 3.1,100GB时写放大为14。原因分析:数据量变大后,数据更有可能从低层级一步步向下压缩迁移至高层级,从而被反复重写多次。
读放大
读放大的来源:
- 为了查找一个 Key-Value 对,LevelDB 可能需要检索多个Level的文件。
- SSTable内部,LevelDB 无法直接精准读取数据块,必须同时读取多个元数据块(Index Block+Bloom Filter Block+Data Block\text{Index Block} + \text{Bloom Filter Block} + \text{Data Block}Index Block+Bloom Filter Block+Data Block)。
例如,查找一个1KB的键值对时,LevelDB需读取16KB的索引块、4KB的布隆过滤器块和4KB的数据块,总计24KB。因此,在最坏情况下考虑全部14个SSTable文件时,LevelDB的读取放大量为24×14=336。对于较小的键值对,读取放大效应会更为显著。
在图2中,1 GB数据量下读放大为8.2,100GB时读放大为327。原因分析:对于小数据量,Index Block 和 Bloom Filter 可以完全被内存 Cache 缓存;但对于 100 GB 的大数据量,内存装不下全部索引,每次查询都会去读取不同 SSTable 文件的 Index Block 和 Bloom Filter,导致读放大扩大。
SSD和LSM-Tree
与机械硬盘类似,出于其独特的“先擦除再写入”循环以及高昂的垃圾回收(GC)开销,随机写入在 SSD 中同样会导致性能下降。这就使得LSM-Tree同样很适合用在SSD上。
但是与 HDD 不同,SSD 拥有极佳的随机读性能和内部高并发能力。
- 单线程随机读:只要请求粒度变大(如 256 KB),吞吐量即可达到顺序读的一半。
- 多线程并发随机读:通过多线程(如 32 线程)并发发起随机读,当请求大小≥\ge≥16 KB 时,吞吐量可以完全媲美顺序读。
WiscKey方案设计
设计目标
WiscKey是从Leveldb派生而来的单机持久化kv存储。提供和Leveldb一样的API(Put,Get,Delete,Scan)。WiscKey的设计目标有下面几个:
- 降低写放大
严重的写放大会导致如下问题:消耗大部分SSD带宽资源;同时因为SSD擦除次数有限,写放大同样会缩短固态硬盘的使用寿命。 - 降低读放大
较高的读放大会导致如下问题:每次查询都需要执行多次读取操作,导致查询吞吐量显著降低;其次,大量数据被加载到内存中会降低缓存的使用效率。 - 针对 SSD 进行优化
WiscKey 通过将其 I/O 模式与 SSD 设备的性能特性相匹配,针对 SSD 设备进行了深度优化。它有效地利用了顺序写入和并发随机读取,从而能够充分利用SSD的带宽。 - 丰富的API支持
WiscKey 旨在支持使 LSM-Tree 流行起来的那些现代特性,例如范围查询和快照。 - 更好支持实际场景key-value大小
在现代工作负载中,Key 的大小通常较小(例如 16 字节),而 Value 的尺寸差异很大(例如从 100 字节到大于 4 KB 不等)。WiscKey 旨在针对这种贴近实际应用的键值尺寸组合提供出色的性能。
kv分离方案
引入kv分离的原因
LSM-Tree的性能开销主要是在compaction过程。compaction需要对多个SSTable进行排序,多个SSTable的内容会读入到内存中排序后在写入SSTable。这会占用很多的磁盘带宽。但是这个排序对于检索kv很重要,不能去掉。
WiscKey的解决方案来源于识别到compaction只要求对key进行排序,但是value可以进行单独的存储管理。这其实需要基于一个数据模型是key比vlaue小很多,如果key比value大很多的话就没必要进行kv分离了。
WiscKey的kv分离存储
WiscKey 只在 LSM-Tree 中存放 Key 和 Value 的物理地址,而将真实的 Value 追加保存在单独的日志文件vLog。
数据操作流程:
- 写入:value先写入vLog中,然后将key和value的addr(<vLog-offset, value-size>))写入LSM-Tree中。
- 删除:删除直接再LSM-Tree中删除对应的key,不会操作vLog中的value。没有LSM-Tree中的key引用的vLog数据是无效的,后续通过GC处理。
- 查询:先查询LSM-Tree,然后根据addr查询对应的value
kv分离带来的好处:
- 显著降低LSM-Tree的大小。
- 降低LSM-Tree的写放大。假设 Key 为16 B16\text{ B}16B,Value 为1 KB1\text{ KB}1KB(1024 B1024\text{ B}1024B), LSM-Tree(的写放大为 10,而
vLog(追加写 Value)的写放大为 1:WiscKey 的有效写放大=10×16+102416+1024≈1.14\text{WiscKey 的有效写放大} = \frac{10 \times 16 + 1024}{16 + 1024} \approx \mathbf{1.14}WiscKey的有效写放大=16+102410×16+1024≈1.14。 - 写放大降低后可以有效提高SSD的寿命。
kv分离对点查的影响:对于一次读需要先读取LSM-Tree获取到value的addr,然后再根据addr的地址从VLog读取value。这比Leveldb多一次磁盘IO,这会导致读比Leveldb慢。但是因为LSM-Tree中只存储了value的addr,这会降低LSM-Tree的大小,可以使得LSM-Tree的数据都缓存到内存中,那么WiscKey的读取只需要一次磁盘IO,这就比Leveldb的读取操作要快了。
kv分离场景下的扫描应该是比Leveldb要慢的,因为value是无序的状态。
kv分离带来的挑战
kv分流带来了以下挑战:
- 范围查询需要随机IO;
- 需要引入垃圾回收;
- 需要处理崩溃恢复时的数据一致性;
范围查询
范围查询的问题:在LevelDB中,键值被共同存储并排序,范围查询可顺序从SSTable文件中读取键值对。然而,由于WiscKey中键值是分别存储的,范围查询需要随机读取数据,这会降低范围查询的速度。
在SSD中单线程的随机读取性能时比不上顺序读取性能的,但是对于请求数据较大的并行随机读取性能时能媲美顺序读取性能的。
解决方案:并发从SSD中进行预读。范围查询时,一次获取多个key和key对应的addr,然后并发的获取多个addr对应的value来提高范围查询的性能。
针对预读的API改造:识别到用户需要连续读取数据后(Next()或Prev()操作);从LSM-Tree中顺序读取多个key-addr;然后将addr写入到队列中;后台多线程并发读取队列中的addr对应的value写入缓存中。
GC
引入GC原因
基于标准 LSM 树的键值存储结构在删除或覆盖键值对时,并不会立即释放空闲空间。而是在compaction过程中,如果发现与已删除或被覆盖的键值对相关的数据,则该数据将被丢弃并释放空间。
在WiscKey中,compaction只会对key进行回收,但是value在compaction的过程中不会回收。所以需要一个单独的GC来对vLog中的无效空间进行回收。
解决方案
一种简单的回收vLog中空闲空间的方法是:首先扫描 LSM 树获取所有有效的值地址;随后,将vLog中未被 LSM 树引用的value视为无效并予以回收。但是该方法耗时过长,仅适用于离线垃圾回收场景。
WiscKey的解决方法:在将值存储于vLog时,同时会一并存储对应的key。vLog的存储格式如下图:
WiskSec的GC的目标是将有效值保留在vLog的连续范围内。该范围的一端(称为head)始终对应vLog的末尾位置,新的数据值将被追加在这个地方;另一端(称为tail)则是垃圾回收机制触发时开始释放空间的位置。只有位于头部与尾部之间的vLog段包含有效数据,并会在查询过程中被检索。
GC流程
WiscKey的GC流程:
- 从vLog的tail读取一大块键值对(例如数兆字节)
- 通过查询LSM-Tree来找出其中有效的key-value对
- 将有效的key-value对搬移到head
- 更新tail,释放空间
为了防止数据丢失,WiscKey必须确保新添加的有效值及新的尾部数据在实际释放空间之前均能持久保存于磁盘上。WiscKey通过下面的手动来保证数据不丢失:
- 在将有效值写入到vLog后,对vLog调用fsync()函数进行落盘
- 将新的vlaue address和tail(tail格式:<‘‘tail’’, tail-vLog-offset>.)以同步方式记录到LSM-Tree中。
- 最后再释放空间
GC执行时机
WiscKey 可以配置为定期启动并持续执行GC,或者达到特定阈值时触发。垃圾回收也可以在离线维护模式下运行。
崩溃恢复时的数据一致性
背景
在系统崩溃时, LSM 树实现机制通常能保证插入的键值对具有原子性,并确保插入对能够按顺序恢复。由于WiscKey架构将数值与 LSM 树分开存储,要实现相同的崩溃保障机制可能会较为复杂(键值对的原子性很难保证)。
WiscKey通过利用现代文件系统(如ext4、btrfs和xfs)的一个重要特性来提供相同的保障:若vLog中的某个值X在崩溃时丢失,则所有后续插入的值也会一同丢失。
数据一致性的实现
用户查询kv对时的数据一致性保证:
- 如果无法在 LSM 树中找到该key,则其行为与传统 LSM 树完全一致。如果value已经写入到vLog中了,对应的value会再GC中进行回收;
- 如果能在 LSM 树中找到该键,则需额外步骤以确保数据一致性:
- WiscKey首先验证从 LSM 树获取的值地址是否处于vLog当前有效范围内。
- 确认所得值是否与查询键一致(通过vLog中保存的key和LSM-Tree中的key进行对比)。如果不一致,那么就从LSM-Tree中删除key。
LSM 树还支持用户明确请求同步插入操作来保证数据持久性。WiscKey通过在向 LSM 树执行同步插入前flush vLog到磁盘上来实现这一功能。
优化
vLog写缓存
对于每次Put()操作,WiscKey都需要通过write()系统调用将数据值追加到vLog中。然而,在以插入操作为主的业务场景下,向文件系统发起大量小数据写入会带来显著的系统开销。
为降低系统开销,WiscKey会在用户空间缓冲区中缓存数据值,仅当缓冲区大小超过阈值或用户请求同步写入时才将缓存区的数据刷入磁盘中。在数据查询时,WiscKey首先会在vLog缓冲区中查找。
这种机制可能导致部分缓存的数据在系统崩溃时丢失。
缓存区要预分配vLog的offest?保证缓存按照顺序写入vLog。
LSM-Tree WAL日志优化
在WiscKey中, vLog会记录已插入的kv以支持垃圾回收机制。因此, LSM-Tree的WAL日志就可以无损去除了。crash后可以通过扫描vLog来进行恢复。
为了减少扫描vLog的范围,WiscKey会定期将vLog的head作为键值对<‘’head‘’,head-vLog-offset>记录在 LSM 树中。
当数据库启动或重新打开时,WiscKey 从 LSM-Tree 中记录的最新head位置开始,向后顺序扫描vLog,直到vLog文件的末尾,恢复出其中的kv对。由于 LSM-Tree 本身能够保证其中记录的 Key 恢复时严格满足按插入顺序恢复的特性,因此基于 LSM-Tree 中的 head 结合 vLog 扫描来进行恢复是可以保证数据一致性的。
这个会导致db recover的时间变长,因为再kv分离的场景下vLog的存储包括value(WAL中只包括key和value的addr),读取的空间会比WAL日志的大。
实现
WiskKey基于LevelDB 1.18版本开发。
WiskSec在创建新数据库时创建vLog,并在LSM-Tree中管理key和value的addr。
vLog 在内部会被具有不同访问模式的多个组件同时访问。例如,查询操作通过随机读取 vLog来完成,而GC则是从 vLog 文件的tail顺序读取,并追加写入到head。我们使用posix_fadvise()来预先声明对 vLog的访问模式。
对于范围查询,WiscKey 维护了一个包含 32 个线程的后台线程池。这些线程在一个线程安全的队列上休眠,等待新的 Value 地址到来。当预读被触发时,WiscKey 会将固定数量的 Value 地址插入到工作队列中,并唤醒所有休眠的线程。这些线程随后开始并行读取 Value,并将它们缓存到 Buffer Cache中。
为了高效地回收vLog 释放出的空间,我们使用了现代文件系统的打孔(Hole-punching)功能(通过fallocate()系统调用实现)。可以在不改变文件总体偏移量的前提下,把vLog头部被 GC 掉的无效数据区的物理磁盘块释放掉,还给操作系统。
现代文件系统支持的最大单文件体积足够大,足以让 WiscKey 运行很长时间而无需回绕至文件开头;例如,ext4 的最大文件大小为 64 TB,xfs 为 8 EB,btrfs 为 16 EB。如果有需要,vLog也很容易改编为循环日志的形式。
性能评估
所有实验均在一台测试机器上运行,该机器配备了两颗 Intel® Xeon® CPU E5-2667 v2 @ 3.30GHz 处理器和 64 GB 内存。操作系统为 64 位 Linux 3.14,使用的文件系统是 ext4。所用的存储设备为一块 500 GB 的三星 840 EVO SSD,其顺序读取和顺序写入的最大性能分别为 500 MB/s 和 400 MB/s。
Microbenchmarks
我们使用db_bench(LevelDB 中默认的微基准测试工具)来评估 LevelDB 和 WiscKey 的性能。在所有实验中,我们统一将 Key 的大小设为 16 字节,但针对不同的 Value 大小进行了多组实验。为了更便于理解和分析性能,我们禁用了数据压缩功能。
写入性能
下面是顺序写入和随机写入微基准测试的结果。前者通过按顺序插入 Key 来构建一个 100 GB 的数据库,而后者则以均匀分布的随机顺序插入 Key。需要注意的是,顺序写入在 LevelDB 或和WiscKey 中都不会触发 Compaction,而随机写入则会触发。
顺序写入性能
Figure 7显示顺序写的场景下,LevelDB和WhisKey的吞吐量均随数据值大小的增加而提升。但是在最大的value大小下,LevelDB的吞吐量也没有达到磁盘的带宽。
Figure 8显示了LevelDB中各组件所耗时间的分布情况。时间主要消耗在三个主要环节:向日志文件写入数据、向内存表插入数据,以及等待memtable刷盘到SSTable。对于小value而言,写入日志文件所占总耗时比例最高;对于大value,等待memtable刷盘到SSTable则是性能瓶颈。
与LevelDB不同,WiscKey在处理超过4KB的数据量时可充分利用磁盘的全部带宽。由于它既不写 LSM-Tree日志,同时在写vLog时使用Buffer缓存,因此即使处理较小的value时速度也提升至原来的3倍。
随机写入性能
图9展示了LevelDB和WiscKey在不同value大小下的随机写入吞吐量。LevelDB的吞吐量范围从仅2 MB/s(64字节值大小)到4.1 MB/s(256 KB值大小)。WiskSec的吞吐量随value大小增加而提升,在数据值大于4 KB时达到磁盘写入瓶颈。WiskSec的吞吐量是LevelD的 46倍到111倍。
LevelDB的吞吐量较低,是因为compaction不仅会消耗很大一部分磁盘带宽,还会减慢前台写入操作(以控制LSM-Tree L0层的增长速度,compaction成为系统瓶颈)。
图10对比了LevelDB和WhisKey在随机写入下的写放大。LevelDB的写放大始终大于12;而当value的大小达到1KB时,WiscKey的写放大迅速降至接近1。
查询性能
下面将比较LevelDB与WiscKey在随机查找和范围查询方面的性能表现。
随机查找性能
运行负载:在随机写入100GB的数据后进行100000次随机查询。
结果分析:对于1 KB的value,WiscKey的吞吐量是LevelDB的12倍;而对于较大的value,WiscKey的吞吐量仅受限于磁盘的随机读取吞吐量(如图 3 所示)。
LevelDB的吞吐量较低,这是由于其较高的读放大和compaction占用磁盘带宽所导致的。
范围查询性能
- 随机写入的场景
LevelDB会从不同层级读取多个文件,而WiscKey则需要对vLog进行随机访问(但WiscKey利用了并行随机读取技术)。
LevelDB的吞吐量最初随数据库值大小的增加而提升。然而,当value超过4 KB时,由于SSTable文件仅能存储少量键值对,其开销主要源于需要打开多个SSTable文件并读取每个文件中的索引块和布隆过滤器。
WiscKey分析。对于较大的value,WiscKey可以达到磁盘的顺序读取带宽,最高可达LevelDB的8.4倍。然而,在64字节的value下,WiscKey的性能比LevelDB差12倍,这是由于达到了SSD并发请求的上限。 - 顺序写入的场景
其性能表现与随机写入的场景呈现相同趋势。
64字节的value大小下,WiscKey的速度比其他value大小慢25%,原因是WiscKey需要同时从LSM-Tree和vLog中读取数据。但对于大value,WiscKey的速度则快2.8倍。
因此,对于小value,通过对vLog排序,可使WiscKey的范围查询性能达到与LevelDB相当的水平。
GC
下面开发分析在后台执行垃圾回收时WiscKey的性能表现。
性能会受到空闲空间百分比的影响,因为这会影响GC线程写入的数据量以及释放的空间量。
性能测试步骤和负载
性能测试具体包含三个步骤:
- 我们通过随机写创建一个数据库;
- 删除所需比例的kv对;
- 运行随机写负载程序,并且在后台GC的同时测量其吞吐量;
使用 4 KB 的键值对大小,并将空闲空间百分比从 25% 到 100% 进行变动。
性能结果与原因
- 100% 空闲空间:吞吐量仅下降 10%。
- 原因:GC 只需从 vLog 尾部顺序读取,无需向头部重写任何有效数据(写开销为 0)。
- 其他比例(25%~75%):吞吐量下降约 35%。
- 原因:GC 线程需要把未被删除的有效数据重新写回 vLog head,抢占了部分写入带宽。
崩溃恢复时的一致性验证和耗时
一致性验证
- 背景与工具:由于键值分离机制增加了维护一致性的难度,论文使用了专业的崩溃验证工具ALICE进行了系统性测试。
- 测试方法:在 ext4、xfs 和 btrfs 文件系统上运行异步和同步的 Put() 请求,同时通过ALICE 模拟了超过 3000 种可能触发数据不一致的系统崩溃场景。
- 结果:ALICE 未发现由 WiscKey 引入的任何一致性漏洞,证明其一致性机制安全可靠。
崩溃恢复耗时
1 KB Value 场景下,LevelDB 的恢复时间为 0.7 秒。WiscKey 的恢复时间为 2.6 秒比 LevelDB 稍慢。%% 随着value变大,需要读取的vLog也会变大,那么WiscKey的恢复时间会变的更长 %%。
空间放大
空间放大是指磁盘上存储的实际大小与用户写入的数据大小之比。压缩可降低空间放大,而额外数据(如垃圾数据、碎片或元数据)则会加大空间放大。为简化讨论,本文未启用压缩功能。
对于顺序写入场景,空间放大倍数可接近1,因为 LSM-Tree中额外的元数据量极小。
对于随机写入和覆盖写入,当无效kv未能被快速地回收时,空间放大倍数通常大于1。
LevelDB空间放大来源:来自于尚未被Compaction清理的旧/无效 Key-Value 键值对。
WiscKey空间放大来源包含两部分:
- vLog 中未被 GC 清理的无效 KV。
- 键值分离带来的额外元数据开销(即 LSM-Tree 中存储的 Value 指针/Offset,以及 vLog中记录的 Header 元数据)。
当存入的 Value 相对较大时(元数据占比微乎其微),经过 GC 后的 WiscKey 磁盘存储容量非常接近真实有效数据的逻辑大小。
没有任何键值存储系统能够同时最小化读放大、写放大和空间放大。不同的系统只是在三者之间做出了不同的取舍:
- LevelDB:
牺牲了更高的写放大(后台频繁读写排序),换取更低的空间放大(及时清理无效数据),但这极大影响了前台写入性能。 - WiscKey:
容忍更高的空间放大,来将 写放大降到最低。由于 GC 可以延迟到后台空闲时再做,从而最大程度减少了对前台写入性能的干扰。
CPU使用率
- 顺序写入:
- LevelDB 消耗 CPU 更高:因为 LevelDB 在将键值对写入 WAL 日志文件时需要进行复杂的编码(处理,CPU 开销较大。
- WiscKey 消耗 CPU 更低:WiscKey 移除了 WAL 日志优化,省去了这部分编码计算。
- 范围查询:
- WiscKey 消耗 CPU 显著增高:因为 WiscKey 启用了 32 个后台线程进行预取数据的并行读取,多线程并发导致 CPU 利用率上升。
实验表明,在测试环境下,CPU 并不是 LevelDB 或 WiscKey 的性能瓶颈(瓶颈依然在 I/O 和磁盘上)。
LevelDB的写入和compaction都是单线程的,所以CPU消耗不高。
YCSB 基准测试
数据与参数:数据库规模为 100 GB,关闭数据压缩,选取 1 KB 和 16 KB 两种 Value 大小。
- 数据写入:
- 1 KB Value:WiscKey 速度是 LevelDB/RocksDB 的 50 倍以上(最坏情况下仍快 45 倍)。
- 16 KB Value:即使在最坏情况下,WiscKey 也比其他两者快 104 倍。
- 读密集型负载(Workload A / B / C):
- Zipf倾斜分布(热点缓存):由于高频访问的热点 Key 被内存缓存,减少了实际磁盘访问,这一定程度缩小了 WiscKey 对比 LevelDB/RocksDB 的优势差距。
- 读比例与优势:WiscKey 在 50% 读取的 Workload-A 中优势更明显,而在 95%~100% 读取的 Workload-B/C 中优势有所收窄。但在所有场景下,RocksDB 和 LevelDB 的性能仍全面落后于 WiscKey。
- 小范围查询负载(Workload E):
- 该负载包含大量检索 1~100 个 KV 的小范围查询。查询每个范围的首个 Key 本质上是一次随机查找,而这正是 WiscKey 的强项。因此,即使在 1 KB 小 Value 场景下,WiscKey 依然显著优于 RocksDB 和 LevelDB。
即使常驻后台 GC(最坏情况),WiscKey 的整体表现依然好于 LevelDB 和 RocksDB。但 GC 机制对不同 Value 的影响侧重不同:
- 该负载包含大量检索 1~100 个 KV 的小范围查询。查询每个范围的首个 Key 本质上是一次随机查找,而这正是 WiscKey 的强项。因此,即使在 1 KB 小 Value 场景下,WiscKey 依然显著优于 RocksDB 和 LevelDB。
- 小 Value(1 KB):一个 4 MB 的 vLog块包含极多 Key-Value,GC 线程需要频繁查 LSM-Tree 验证每个键值对是否有效,索引验证耗时开销较大。
- 大 Value(16 KB):单个块内 KV 数量少,验证耗时极短,GC 线程会更快地向磁盘重写清理后的数据,从而更直接地抢占前台写入的磁盘 I/O 吞吐。 _(注:若有需要,可通过限流来削弱 GC 对前台性能的影响)
相关工作
基于哈希表的 SSD 键值存储
- 相关系统:FAWN、FlashStore、SkimpyStash、BufferHash、SILT。
- 技术特点:采用追加写日志(Log-Structure)存数据,在内存中构建哈希表做索引。
- 与 WiscKey 对比:虽然 WiscKey 借鉴了其追加写日志的数据布局,但哈希表索引无法高效支持范围查询和快照;而 WiskKey专注于开发一款功能丰富的键值存储库,可适用于多种场景。
LSM-Tree 的优化与演进
- 相关系统:
- bLSM:优化 Merge 调度以稳定写入延迟;
- VT-tree:通过间接指针层规避 Compaction 中对已有序数据的重复排序;
- LOCS:暴露闪存内部通道以实现并行 Compaction;
- Atlas:分布式架构,将 Key 和 Value 分开存放在不同硬盘;
- LSM-trie:使用 Trie 树组织 Key 以提升合并效率(但牺牲了范围查询);
- RocksDB:做了大量多线程和内存优化,但底层依然是传统 LSM-Tree 架构。
- 与 WiscKey 对比:
- 之前的很多优化(如 RocksDB)与 WiscKey 的设计是正交(互不冲突、可叠加)的。
- WiscKey 采用直接的键值分离,从根本上极大地降低了写放大。
类似键值分离理念的混合/文件系统
- 相关系统:Walnut(大对象存文件系统)、IndexFS(元数据用 LSM-Tree 存)、Purity(索引排序,元数据按时间存)。
- 与 WiscKey 对比:虽然思路相似,但 WiscKey 的解决方案更加通用和完整,并且专门针对 SSD 设备在各种复杂工作负载下的“加载”和“查找”性能做了深度优化。
基于其他数据结构的键值存储
- 相关系统:TokuDB(分形树/Fractal-Tree)、ForestDB(HB±trie 树)、NVMKV(结合 FTL 原生特性)、Vector Interfaces(批量向量接口)。
- 与 WiscKey 对比:这些系统都是基于不同的数据结构,它们在性能方面各自存在不同的权衡;而 WiscKey 致力于直接改进应用最广泛的传统 LSM-Tree 结构。
纯内存键值存储优化
- 相关系统:Masstree、MemC3、MICA、cLSM 等(主要解决内存中的并发与扩展瓶颈)。
- 未来展望:这些纯内存系统的并发与伸缩技术可以无缝引入并结合到 WiscKey 中,以进一步提升性能。
总结
键值存储已经成为数据密集型应用中不可或缺的基石。在本文中,我们提出了 WiscKey——一种全新的基于 LSM-Tree 的键值存储系统,它通过将 Key(键)与 Value(值)分离来最小化写放大和读放大。WiscKey 的数据布局与 I/O 模式针对 SSD 设备进行了深度优化。我们的实验结果表明,WiscKey 能显著提升绝大多数工作负载下的性能。我们希望 WiscKey 中的键值分离技术及各项优化手段,能为下一代高性能键值存储系统的设计带来启发。