论文:REMIX: Efficient Range Query for LSM-trees

519 阅读11分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

开篇

今天我将介绍我们的论文:Remix,LSM树的高效范围查询。fast21-zhong.pdf (usenix.org) 本文是我逐句听写翻译而来,本文花了我很多时间去理解和听译。

LSM树是许多数据库系统的骨干。这些系统是许多现代应用的构件,同时LSM树本质上是一种优化数据结构的权利,它在内存缓冲区内缓存更新,当缓冲区填满后

它对更新进行合并,并将其Flush到持久性存储。lsm树将数据组织在不同(指容量)大小的Levle上,这些Level通常相互重叠,一个Level的数据将通过批量写入更新而逐渐压缩到下一个层次。lsm树比传统的索引结构(如B3)实现了更好的写入效率,这取决于如何选择数据进行Compaction。

关于compaction对性能的影响,怎么形容都不为过,下篇我讲发一篇关于LSM compaction的设计空间的论文讨论。

我们有不同的compaction策略。一般来说,有两种类型的compaction策略。tiered compaction and level the compaction. 在tiered compaction中,每一层都会从上一层积累运行。当runs达到阈值时,这些runs将被淹没并冲到下一级。在tiered compaction中,每一层都有正好一个排序的runs,来自上一层的更新将被合并,形成一个新的runs。当runs的大小达到阈值时,它将被flush并与下一级的运行合并。因层级的compaction是比较aggressive地将更新与现有数据合并,每层的数据将被多次重写。另一方面,通过在一个Level中积累更新,并一次性sort merge它们,tiered compaction的内耗(insight cost,指的是写放大?)成本较低。

让我们来看看LSM树是如何执行查询的。对于点查询,LSM树通常采用Bloom过滤器,以避免访问不包含搜索key的条目,在实践中,大多数查询可以只用一个IO即可完成。在传统的基于LSM的系统中,范围查询是使用merge iterator实现的,该迭代器本质上是一个min-heap结构,它合并了所有的轮次以提供一个排序的视图。因为LSM树中的runs经常重叠,所以范围查询必须访问存储中的每个runs。如果rounds不多,这听起来问题不大。然而,由于tier compaction 维护了多个sort runs以提高写入效率。合并迭代器需要访问更多的runs,这导致了更糟糕的范围查询性能。不同的compaction策略本质上是在读和写的效率之间做不同的权衡。

下图形象展示了不同诉求(读写性能)下对compaction的选择。看起来只能二选一?

image-20220202184329259

我们能否同时实现高性能的读和写,因为level compaction 积极地合并少量数据,以维持每一层级Level的run。在这个模型的基础上,我们没有什么办法改善写。

因为compaction占据了太多资源,但结果确实每次compaction的是少量的数据(没有大批量去做)。

另一方面,tiered compaction是以批处理的方式去merge runs,这同时也提供了高性能的写入,它维护了多个runs的数据,导致了较慢的范围查询。所以我们认为,即使我们有很多runs,我们能否实现更快的范围查询?

作者希望在tiered compaction提供高性能写的基础上,去改进快速查询。

image-20220202194633025

为什么范围查询可能非常昂贵?

首先,让我们看看为什么使用min-heap实现的范围查询迭代器会很昂贵。假设我们有三个sort runs,这些数字代表运行中的Key。

image-20220202195409673.png

上图展示了一个范围查询的例子,我们在范围查询30和50之间的key。

  • 在每个round中二分查找第一个大于30的item。
  • 经过多次min-heap堆排,找到最小的item是31-R2;

接下来,我们应该在R2上优先使用31这个游标。然后更新游标下一个值43给堆,变成这样,于是输出31,43,注意到R2下轮是52>max,范围查找结束。

注意,step1 找到:{R0:71,R1:73,R2:31},给min-heap后只有R2合适,继续在R2上往前next.直到52。

image-20220202220012887

第一步是用二分查找来寻找每个Round,这样游标就会指向最小的大于或等于begin的key。接着,我们比较这些key,并填充min-heap,于是我们把最小的key 在R2的key:31放在min-heap中,R2上的游标应该提前跳,我们的得到了43,然后我们更新min-heap并输出Key:43,因为,这仍是所有最新游标中下最小的key。 类似地,为了检索下一个Key,R2上的游标应该往前跳过Key43,以便重复再次比较,并输出min的key:52,已经>我们要找的50,范围查询结束。

最小堆的成本可以总结为以下几点。

  • 首先,初始化min-hit需要在每次运行时进行二分查找。

  • 其次,当推进迭代器时,需要进行多次key比较以调整堆并输出最小的key

  • 最后,它需要访问每个run,即使其中一些轮次没有覆盖范围内的Key。

在我们之前的例子中,key{31,43,52}只属于R2,即使只是属于R2,但我们仍然需要访问R0和R1。

我们还有两个观察结果,首先,排序的视图是稳定的。例如,这三个sort run的排序视图总是从2 4 6开始,在91结束。我们发现这个查找是稳定排序。

只要runs保持不变,排序的视图就保持不变。然而,排序的视图是在运行时构建的,然后在范围查询结束后丢弃,以避免重复重建排序的视图。我们可以记录视图的访问路径。

优化点是不要重复重建sort view.相当于每次merge复用上次的结果。

image.png

image.png

LSM树不适应现代磁盘?

lsm树最初是为旋转磁盘设计的,数据必须进行物理排序才能高效地访问。今天,ssd的随机访问性能有了很大的提高,cpu已经成为瓶颈,保持数据的物理排序可能并不划算。

(Remix的)目标是记录和重用sorted views。compaction时,不需要物理地执行sort merge.

image-20221206165159222

总结:基于这些观察,我们提出了高效的多表索引。简而言之,remix有效地记录了LSM TREE中的排序视图。remix元数据包含三个部分:

首先,运行选择器记录了排序后的视图的顺序访问路径,如果我们想访问视图中间的任何Key,只使用run选择器就足矣。

例如下图中,run selector记录了从0 2 1... round中得到了想要的元素。但是,还不够,如果只有run 选择器,每次得从这个Rn中从开开始扫描。于是增加了segment的概念。

image-20221206165331118

我们将不得不从头开始扫描,为了避免这种情况,我们将视图分为段。在这个例子中,每个段中最多有四个Key,以快速定位一个段。我们还将最小的Key存储在段中,作为锚定Key。以便于扫描一个段,我们还存储了每个段的游标偏移量,记录了迭代器的位置,就像我们要从锚定Key开始扫描一样。

所谓segment就是根据Run selector记录的R下标组成的一系列元素,下图中有4个元素/段

image-20220202221801075

上图中,例如,让我们看一下第二个segment,key 11是这个段中最小的Key,也是锚Key,游标偏移量指向key11,17和31是大于或等于Key11的最小的Key,对应于Key11的合并迭代器的状态。

让我们看一个使用remix检索15和28之间的范围的例子,范围之间的Key将被复制到用户缓冲区。

因为LSM会先load 对应的block cache

第一步是找到一个覆盖起始Key的目标段。Key 15在11和31之间,所以选择第二段。(类似二分查找定位到了segment 2)

下一步是初始化游标。根据游标偏移量是1 2 1 ,所以游标将在三轮中被定位为指向11 17和31。

迭代器的当前指针将被设置为指向该段的第一个run选择器,在这种情况下,迭代器将指向r0,根据重建的游标(从remix结构重建)偏移量,它对应于key:11,然后我们比较游标下的Key,Key11比Key15小,所以我们需要推进迭代器来找到范围内的第一个Key。

为了推进迭代器,我们首先推进当前运行的游标,在这种情况下,r0的游标将被推进指向23号Key,接下来,我们直接从Run selector选下一个selector(不必去读和比较!之前已经比较过了。)

然后当前指针也将被推进,指向本段的下一个运行选择器。然后迭代器将指向r1的游标下的Key,也就是Key17,17大于15,所以Key17被复制到用户缓冲区。我们不断地推进迭代器来获取范围内所有的Key。 同样,r1的游标将被推进到Key29的位置。然后迭代器将指向r0的游标下的Key,也就是Key23,Key23将被复制到用户缓冲区。我们继续推进迭代器,r0的游标将提前指向Key71,然后迭代器将指向r1中大于29的Key,所以范围扫描结束了。

让我们总结一下使用remix的好处。

首先,remix使用二进制搜索锚Key来初始化一个迭代器。

第二,使用remix推进迭代器,不需要任何Key的比较。

最后,remix可以跳过访问不在搜索路径上的运行。

我们的论文中阐述了remix的更多好处。

image.png

与remix相结合,我们建立了remixdb和基于LSM树的键值存储,remixdb使用一层(level)的分区布局,这被证明是在现实世界的工作负载下是有效的。

LSM 一层? 不太可能

与传统的LSM树类似,当内存缓冲区填满时,更新将被刷入相应的分区。被刷新到持久化存储中的相应分区。在每个分区中,根据新数据的大小和旧数据的大小,可以有三种压缩方式。

image-20221206173122632

  • minor compaction直接将新的table文件放在旧的table文件之上。(类似L0压缩?)
  • major compaction选择几个相似大小的table文件,并将它们压缩成一个。
  • split compaction将一个较大的分区分成多个新的分区。

所以,让我们看看remix和remixdb的实际表现。我们用一个micro bechmark和YCSB基准来评估remix和remixdb。

在micro benchmark中,我们将remix与min-heap合并迭代器进行了比较,我们使用1到16个表文件进行了一组实验,在每个实验中,键被随机分配给不同的table文件。

当只有一个table文件时,两种方法都使用二分搜索,而remix的恒定开销略高。

image-20220202223533542

当有多于一个表文件时,remix比合并迭代器快。当有16个表文件时,remix实现了高达9.3倍的速度。这里我们显示了寻址操作的结果,然后是扫描下一个50个键。扫描在复制键上带来了持续的开销。在这种情况下,remix在有5个表文件的情况下仍然实现了大约2倍的速度,在有16个表文件的情况下实现了3倍的速度。

为了评估remixdb的性能,我们将remix db与leveldb rocksdb和pebblesdb进行了比较。我们给每个系统加载256GB的随机顺序的键值对,然后运行ycsb工作负载a到f。remix db的低吞吐量比pebblesdb快30%左右,pebblesdb也采用了tiering压缩策略。在e项目大多包含查询范围的情况下,remixdb与使用分级压缩的系统相比,实现了高达2倍的速度。

ycsb工作负载d主要执行点查询,对最近插入的键,大部分的查询可以从内存缓冲区中返回。因此,remixdb在此工作负载下没有显示出明显的改进。

综上所述,remix记录并重用排序视图,以支持对多个排序运行的快速范围查询。我们在remix的基础上建立了一个remix db,它通过采用单级分区布局和remix db实现了高效的读写。我们在remix的基础上建立了remix数据库,通过采用remix的单级分区布局实现了高效的读写。

image-20220202224211201