一种残膜回收机防缠绕挑膜装置的制 一种秧草收获机用电力驱动行走机构

管理存储在非易失性存储器中的键值对的LSM树的制作方法

2022-03-09 04:26:34 来源:中国专利 TAG:

filter)等。它们的承诺在于它们支持在其指纹旁边存储对于每个条目的辅助和可更新信息。因此,能够用一个可更新的ff替换lsm树的多个bf,该ff将每个数据条目映射到一个指纹和一个辅助run标识符(run identifier,也称为run id或runid)。在应用读取期间,指纹匹配或取消目标键,而run id指示哪个run搜索每个指纹匹配。
14.因此,ff相比bf需要少得多的内存i/o来查找给定条目的run。
15.与需要大量内存i/o的bf不同,ff允许利用实际上恒定数量的内存i/o来找到条目的目标run。
16.由于ff在恒定的访问时间中实现探测,因此它们有望用常数来代替跨多个bf的内存i/o。
17.缩减误报(scaling false positive)和更新
18.利用ff进行lsm树过滤存在两个挑战。
19.首先,对于ff的误报率(fpr)无法很好地缩减(scale)。
20.这是由于run id导致的,它必须随着数据量而增长,以唯一地标识更多run。
21.假设固定内存预算,run id必须随着数据的增长从指纹中“窃取”位(bit)。这增加了fpr并导致更多的储存器i/o。
22.第二个挑战是高效地保持ff内的run id是最新的。可能的解决方案是向储存器发出读取的命令,以在写入之前检查条目是否存在,并且如果存在,则更新其run id。
23.然而,对储存器的额外的读取i/o是昂贵的。总之,基于ff的现有设计无法缩减储存器i/o,而基于bf的设计无法缩减内存i/o。
24.附图简述
25.结合附图,从下面的详细描述中将更全面地理解和领会本公开的实施例,其中:
26.图1示出了维护成本和数据量的示例;
27.图2示出了lsm树变体的示例;
28.图3示出了chucky的高级体系结构的示例;
29.图4示出了霍夫曼树(huffman tree)的示例;
30.图5示出了压缩的结果的示例;
31.图6示出了run id平均大小和lsm树大小比t之间的关系的示例;
32.图7示出了run id的压缩和组合run id(combination run id)的压缩的示例;
33.图8示出了run id平均大小和由单个组合run id表示的run id的数量之间的关系的示例;
34.图9示出了对齐问题和解决方案的示例;
35.图10示出了使用可延展指纹(malleable fingerprinting)和流体对齐编码(fluid alignment coding)的益处的示例;
36.图11示出了第一管理数据结构(mds)的大小和lsm树的层级数之间的关系的示例;
37.图12a-12h示出了chucky和其他方法的性能之间的比较;
38.图13是方法的示例;
39.图14是方法的示例;
40.图15是写入当前run(current run)并更新第一mds的示例;
41.图16是合并操作和更新第一mds的示例;
42.图17是第一mds的示例;
43.图18是第一mds的示例;
44.图19是第一mds的示例;
45.图20是桶对齐(bucket alignment)问题和用于解决对齐问题的一个或更多个解决方案的示例;
46.图21包括第一mds的示例且也包括第二mds的示例;和
47.图22包括第一mds和一个或更多个附加mds的示例。
48.示例实施例的描述
49.在以下详细描述中,阐述了许多具体细节以便提供对本发明的透彻理解。然而,本领域技术人员将理解,可以在没有这些具体细节的情况下实践本发明。在其他情况下,没有详细描述公知的方法、过程和部件,以免模糊本发明。
50.关于本发明的主题在说明书的结束部分被特别指出并被清楚地要求保护。然而,当与附图一起阅读时,通过参考以下详细描述,可最好地理解本发明的关于操作的方法和组织以及其目的、特征和优点。
51.将理解的是,为了说明的简单和清楚,图中所示的元素不一定按比例绘制。例如,为了清楚起见,一些元素的尺寸可能相对于其他元素被放大。此外,在认为适当的情况下,参考数字可在多个图中重复以指示对应的或类似的元素。
52.因为本发明的所示实施例在很大程度上可以使用本领域技术人员已知的电子部件和电路来实现,所以对细节的解释程度将不会比如上所述的必须考虑的程度更大,以便理解和领悟本发明的基本概念,并且以便不混淆或分散本发明的教导。
53.说明书中对方法的任何引用都应当经必要修改而应用于能够执行该方法的设备或系统和/或存储用于执行该方法的指令的非暂时性计算机可读介质。
54.说明书中对系统或设备的任何引用应当经必要修改而应用于可以由系统执行的方法,和/或可以经必要修改而应用于存储由系统可执行的指令的非暂时性计算机可读介质。
55.说明书中对非暂时性计算机可读介质的任何引用都应当经必要修改而应用于能够执行存储在非暂时性计算机可读介质中的指令的设备或系统,和/或可以经必要修改而应用于执行指令的方法。
56.可以提供任何附图、说明书的任何部分和/或任何权利要求中列出的任何模块或单元的任何组合。
57.说明书和/或附图可以涉及处理器。处理器可以是处理电路系统。处理电路系统可以实现为中央处理单元(cpu)和/或一个或更多个其他集成电路,诸如专用集成电路(asic)、现场可编程门阵列(fpga)、全定制集成电路等或这些集成电路的组合。
58.可以提供说明书和/或附图中示出的任何方法的任何步骤的任何组合。
59.可以提供任何权利要求的任何主题的任何组合。
60.可以提供说明书和/或附图中示出的系统、单元、部件、处理器、传感器的任何组合。
61.术语压缩和编码以可互换的方式使用。
62.指纹过滤器(ff)是管理数据结构(mds)的示例,它可以存储指纹和run id。
63.可以提供一种用于以高效的方式管理存储在诸如ssd存储器的非易失性存储器中的lsm树的方法、系统和计算机可读介质。
64.可以提供一种以适当方式缩减指纹的误报率的方法。
65.提供了将ff内的run id高效地保持为最新的方法。例如,即使是在以下情况也成立:不存在针对储存器的额外读取,以在写入之前检查条目是否存在,并且如果存在,则更新其run id。
66.可以提供两种方法的组合。
67.为了解释的简单,下面的大部分文字将涉及一个解决方案。解决方案的示例被称为chucky-霍夫曼编码的键值存储。应该注意的是,霍夫曼编码可以由另一个可变长度代码代替。
68.为了缩减误报,已经发现run id是极大地可压缩的。原因在于它们的分布是近似几何的,这意味着具有较大级别的run id的条目相比具有较小级别的run id的条目极为常见。这允许用更少的位编码更大的run,并且用更多的位编码更小的run。节省的空间可以专用于指纹,以便在数据增长时保持指纹较大。
69.为了缩减更新,已经发现run id可以在合并操作期间适时地更新,同时目标条目被记在内存中。因此,我们可以使run id保持最新,而无需引入任何额外的储存器i/o。
70.已经发现chucky可以同时缩减内存和储存器i/o。它通过用带有压缩的run id的单个ff替换bf来实现这一点,这些压缩的run id在合并操作期间更新。
71.以下文本将说明使用霍夫曼编码进行run id压缩的示例,同时确定并解决由此带来的挑战:(1)如何在ff的桶内对齐指纹和压缩的run id,以及(2)如何高效地编码和解码run id。
72.chucky可以使用通过压缩保存的位来保持指纹较大,从而在数据增长时保证可缩减的误报率。
73.chucky可以适合任何ff,仅举个示例,说明了如何针对ff定制chucky,诸如布谷过滤器。
74.在说明书中,显示了如何用带有辅助run id的ff替换bf,这些run id在合并的同时会适时更新。run id辅助性的,因为它包括run id和指纹。
75.在说明书中,显示了run id是极其可压缩的,并且我们研究了如何使用霍夫曼编码最小化它们的大小。
76.在说明书中,显示了如何在ff桶内对齐压缩的run id和指纹,以实现良好的空间利用率。
77.在说明书中,显示了如何高效地编码和解码run id。
78.在说明书中,显示了如何将chucky与布谷过滤器集成在一起。
79.在说明书中,实验表明chucky同时在内存i/o和储存器i/o方面进行缩减。
80.图1包括曲线图11和12,其示出了chucky的性能与现有技术解决方案之间的比较。
81.通过分析run id的信息理论熵(entropy),本说明书说明了run id是极其可压缩的,并且我们可以通过对一系列run id进行排序并为它们分配单个代码来进一步降低熵,从而实现更大的可压缩性。
82.本说明书说明了一种称为多项霍夫曼的压缩变体,该变体基于run id的给定组合
在桶中重合的概率将霍夫曼代码分配给桶。
83.在说明书中,显示出,压缩的run id引入了桶溢出的问题,并且我们引入了两种技术来解决这个问题,即可变最小有界指纹(variable minimally bounded fingerprint)和剩余霍夫曼(leftover huffman),一种基于指纹后桶中剩余空间分配代码的方法。
84.在说明书中,显示了如何支持lsm树的重复条目的更新,而不会在布谷过滤器中造成无限递归链。
85.chucky可以推广到许多广泛使用的lsm树设计,其适用于不同的应用工作负载。
86.说明书中显示了如何在电源故障后恢复布谷过滤器。
87.lsm树由指数增长的容量的多层级组成。层级0是内存中缓冲区(和/或储存器的第一层),而所有其他层级都在储存器中。应用将键值对插入缓冲区。当缓冲区达到容量时,其内容被刷新为排序数组(sorted array),排序数组称为到储存器中的层级1中的run。
88.可以实施各种合并策略。第一合并策略是指顺序合并,其中,只要给定的层级i达到容量,其run就会合并到层级i 1。然后,层级i 1可以取代层级i,并可被视为修改后的i层级。
89.为了合并run,它们的条目被从储存器带到内存以进行排序合并,然后作为新run被写回到储存器。层级数l是log
t
(n),其中t是任意两个相邻层级之间的容量比,n是总数据量和内存中缓冲区的大小之间的比率。
90.另一种合并策略可以称为多级合并,其包括一次合并多个层级的run。这可能出于各种原因而发生,例如,当预测某个合并将导致某个层级几乎被填满时发生。这种合并策略可以称为预测合并。
91.图表0提供了在整篇文章中用来描述lsm树的不同列表术语的示例。
92.这些术语是:
93.术语定义llsm树的层级数tlsm树的大小比n数据量与缓冲区大小的比率k对于较小层级的合并触发阈值z对于最大层级的合并触发阈值h布隆过滤器散列函数数r预期点读取i/o成本m过滤内存预算
94.表0
95.通过将具有更新值的键值条目插入缓冲区(对于删除,该值是墓碑(tombstone))来执行非就地(即异位,out-of-place)的更新和删除。每当两个run在包含两个具有相同键的条目的同时合并时,较旧的条目被丢弃,因为较新的条目会取代较旧的条目。为了始终找到条目的最新版本,应用读取会遍历各个层级中从最新到最旧的run,并在它找到具有匹配键的第一个条目时终止。如果它的值是墓碑,则读取会向应用返回否定结果。对于储存器中的每一个run,内存中都有栅栏指针(fence pointer)数组,它包含每个数据块处的最小/最大键,从而允许用一个储存器i/o找到run内的相关块。
96.lsm树设计空间跨越了许多有利于不同应用工作负载的变体。最常见的两种是分级合并(leveling)和分层合并(tiering)(分别在rocksdb和cassandra中默认使用)。这在图2中被示出。
97.使用分级合并时,合并在每个层级内贪婪地执行(即,一旦新run开始就执行)。因此,每个层级最多有一个run,并且每个条目在每个层级内平均合并大约t/2次。
98.使用分层合并时,合并在每个层级内惰性地执行(即,仅当层级填满时才执行)。因此,每个层级最多有大约t个run,并且每个条目在每个层级上被合并一次。
99.分级合并更有利于读取和空间优化,而分层合并更有利于写优化。可以改变大小比t来微调这种折衷。
100.图2还显示了惰性分级合并(lazy leveling),这是一种混合模式,在最大层级使用分级合并,而在所有较小层级使用分层合并,以在两者之间提供有利的折衷(即,用于主要是点读取(point read)的空间敏感的写入密集型应用(space-sensitive write-heavy application))。
101.最近的dostoevsky框架使用以下两个参数来概括这三种变体:(1)关于在触发合并之前在最大层级处的run数量的阈值z,以及(2)关于在触发合并之前在每个较小层级处的run数量的阈值k。
102.图2以及表1和表2显示了如何设置这些参数以假设三种设计中的每一种。
103.表1
[0104] 分级合并惰性分级合并分层合并探测成本o(l)o(l-t)o(l-t)构建成本o(l-t)o(l t)o(l)
[0105]
阻塞布隆过滤器内存i/o
[0106]
表2
[0107] 分级合并惰性分级合并分层合并统一(uniform)o(2-m*ln(2)
*l)o(2-m*ln(2)
*l*t)o(2-m*ln(2)
*l*t)最优o(2-m*ln(2)
)o(2-m*ln(2)
)o(2-m*ln(2)
*t)
[0108]
布隆过滤器误报率复杂度
[0109]
例如,参见等式(1),该等式(1)将ai表示为在层级i的最大run数量,以及将a表示为系统中最大run数量,如下关于这些参数。
[0110]
(1)对于1和l之间的i,ai=k;对于i的任何其他值,ai=z。
[0111]
a=ai=(l-1)*k z之和(对于1和l之间的i)。
[0112]
例如,chucky可以构建在dostoevsky之上,能够跨越多个lsm树变体,以适应不同的工作负载。
[0113]
虽然有些设计(如hbase和cassandra)一次合并全部run,但其他设计(如rocksdb)将每个run分区到多个名为有序字符串表(sst)的文件中,并以sst的粒度进行合并。这允许更精细地控制如何在空间和时间上调度合并开销,但是这增加了写放大。为了便于阐述,说明书将合并说明成就好像它是以run的粒度发生的,但是该工作也适用于依赖于用于合并的sst的设计。
[0114]
我们使用rocksdb的动态层级大小适配技术,该技术基于在最大层级处的条目数
量设置层级1到l-1的容量,以限制存储空间放大。
[0115]
我们假设抢占式合并(preemptive merging),由此我们检测到层级1到i何时接近容量,并一次性合并它们的所有run,而不是让合并递归地在层级之间缓慢进行(trickling)并导致更多的写放大。
[0116]
布隆过滤器—在lsm树中的每个run具有相对应的内存中布隆过滤器(bf),布隆过滤器是节省空间的概率数据结构,其被用于测试键是否是集合的成员。所有布隆过滤器都是持久存储的,以便在系统出现故障时可以恢复。bf是带有h散列函数的位数组。对于插入的每个键,我们使用每个散列函数将其映射到h个随机位,将它们设置为从0到1或保持设置为1。检查键的存在需要检查它的h位。如果任何一位设置为0,我们就得到否定。如果都设置为1,我们就得到真或误报。误报率(fpr)是2^(-m*ln(2)),其中m是每个条目的位数。
[0117]
随着我们增加m,位冲突(bit collision)的概率降低,并且因此fpr下降。在业内的kv存储(如,rocksdb)中,每个条目的位数通常设置为十。
[0118]
bf不支持删除(即通过将位重置回0),因为这可能会导致漏报(false negative)。出于这个原因,针对由于合并而出现的新的run从头开始创建新的bf。
[0119]
bf需要h次内存i/o来进行插入和肯定查询。对于否定查询,平均需要两次内存i/o,因为大约50%的位被设置为零,且因此在导致零之前检查的预期位数是二。
[0120]
为了优化内存i/o,已经提出了连续bf的阵列,每个有高速缓存线(cache line)的大小。通过首先将键散列到构成bf中的一个,且然后将键插入其中,来插入键。对于任何插入或查询,这只需要一次内存i/o。代价是fpr略有增加。
[0121]
rocksdb最近从标准bf切换到了阻塞bf。在本文中,我们使用这两种方法作为基线(baseline),并且我们更关注阻塞bf,因为它们是更激烈的竞争。
[0122]
对于具有阻塞bf的lsm树,应用查询最多消耗o(k*(l-1) z)次内存i/o(即每个run的过滤器一次内存i/o)。
[0123]
另一方面,应用更新消耗均摊(amortized)内存i/o为o(t/k*(l-1) t/z)(一个条目被合并并因而被插入新bf的平均次数)。
[0124]
表1总结了lsm树变体中的每个变体的这些成本。
[0125]
观察到,这两个成本指标都随着层级数l的增加而增加,且因此也随着数据量的增加而增加。
[0126]
第二,观察到这些指标之间的相反关系:我们将lsm树的合并设置得越贪婪(即,通过更改合并策略或通过微调大小比),探测成本会随着bf的减少而降低,而构建成本会随着bf的更贪婪地重建而增加。
[0127]
因此,不可能在不降低其他指标的情况下改进这些指标中的一个指标。图1从概念上说明了这种关系。
[0128]
在业内的kv存储为所有层级的bf设置了统一的每条目的位数。
[0129]
然而,这种方法最近被认为是次优的。
[0130]
最优方法是从最大层级重新分配每个条目大约1位,并使用它为较小层级的过滤器线性分配每条目更多的位。
[0131]
虽然这略微增加了最大层级的fpr,但在较小的层级,它以指数形式减少了fpr,使得fpr的总和较小。
[0132]
等式2和3表示了关于这两种方法的fsr:
[0133]
(2) fpr
统一
=2-m*ln(2)
*(k*(l-1) z)
[0134]
(3) [0135]
等式(2)的直觉是,随着数据的增长,fpr因存在更多的run且因而存在更多的bf而增加,在更多的bf中可能出现误报。
[0136]
另一方面,等式(3)表明,利用最优方法,内存和fpr之间的关系与层级数无关,且因此与数据量无关。原因是,随着lsm树的增长,较小的层级被分配了指数式变小的fpr,从而导致fpr的和收敛。
[0137]
我们在表2中总结了相对应的fpr复杂度,并在图1中从概念上将其可视化。
[0138]
虽然我们的主要目标是提高bf的内存带宽,但我们也必须至少将fpr的可缩减与最优bf方法相匹配,以便在所有性能方面具有竞争力。
[0139]
指纹过滤器(ff)是最近作为布隆过滤器的替代出现的一系列数据结构。ff的核心是存储键指纹的紧凑散列表,其中指纹是通过散列键获得的f位串。为了测试集合成员资格,ff将讨论中的键散列到桶中,并将其指纹与桶中的所有指纹进行比较。如果存在匹配,我们得到肯定。ff不能返回漏报,且它返回漏报的概率至少为2-f

[0140]
指纹大小f决定准确性和空间之间的折衷。已经提出的各种ff在它们的冲突解决方法上有所不同,它们在桶之间交换条目来解决冲突。例如,布谷过滤器使用布谷散列的变体,而商过滤器使用线性探测的变体。
[0141]
虽然不同的冲突解决方法给出了不同的ff有细微差别的性能和空间属性,但迄今为止,所有的ff在我们的问题上都有一组共同的期望属性。首先,在内存占用与布隆过滤器相似的情况下,它们支持在几乎恒定的时间内进行查询和更新。其次,与布隆过滤器不同,ff支持在每个条目的指纹旁边存储每个条目的可更新辅助数据。这些能力允许用单个ff替换lsm树的多个布隆过滤器,该单个ff从数据条目映射到数据条目驻留在lsm树中的run。这种设计承诺允许用少量且恒定次数的内存i/o找到条目的目标run,而不像布隆过滤器那样需要在众多过滤器中有至少一个内存i/o。
[0142]
尽管有这样的承诺,但这种方法带来了两个挑战。第一个挑战是当条目在lsm树中合并时,如何保持run id是最新的。第二个挑战是如何随着数据量的增长而保持run id的大小适中。
[0143]
案例研究—最近的slimdb系统是第一个将lsm树与ff集成的系统。因此,它为应对上述两个挑战提供了有趣的案例研究和基线。
[0144]
为了保持ff中的run id是最新的,slimdb为每个应用更新执行对储存器的读取i/o,以检查条目是否存在,并且如果存在,则在ff中更新其run id。这涉及到储存器i/o方面的大量开销,特别是对于执行盲写的应用。
[0145]
其次,slimdb使用二进制编码表示run id。因此,每个run id包含log2(k*(l-1) z)位,以唯一地标识所有run。因此,随着层级数l的增长,需要更多的位。这对于slimdb来说不是问题,因为它是为内存预算受限较少的系统设计的。事实上,slimdb使用额外的内存,通过在内存中存储冲突指纹的完整键来完全防止误报。slimdb还提出了一种新颖的栅栏指针格式。
[0146]
相比之下,我们关注的是每个条目具有更紧凑的m位预算的应用,其中m是不增加的小常数。
[0147]
在这种约束下,等式(4)表示了与每个条目的位数m和run id大小d有关的单个条目的fpr。
[0148]
(4) fpr>2-f
=2-m d
[0149]
通过代入run id大小d,下限简化为2-m
*(k*(l 1) z),这意味着fpr随着层级数的增加而增加,因为run id从指纹中窃取位。
[0150]
chucky是同时缩减内存和储存器i/o的基于lsm的kv存储。它通过用指纹过滤器替换布隆过滤器并在两个方面进行创新来实现这一点。
[0151]
在合并操作期间,chucky会适时保持ff中的run id为最新,而无需额外的储存器i/o成本。此外,它允许跨合并操作继承run id,以避免ff更新,且从而减少内存i/o。通过这种方式,chucky缩减和分离更新和查询ff的成本,如图1所示。
[0152]
chucky可压缩run id,以防止它们的大小随着数据的增长增加并从指纹中获取位。因此,chucky缩减了fpr,且从而缩减了储存器i/o,如图1所示。
[0153]
为了通用性和易于阐述,我们现在抽象化ff的冲突解决方法的细节。
[0154]
图3示出了chucky的体系结构,它使用管理数据结构(mds)来将lsm树中的每个物理条目映射到一个mds条目,该mds条目可包括指纹和run id。该图还分别用实线和虚线示出了查询和更新工作流。
[0155]
在图3中,键k1、k2和k3驻留在不同的run中,但碰巧被ff的散列函数映射到同一个ff桶。键k2和k3具有冲突的指纹y,而键k1具有不同的指纹x。应用查询键k3,因此我们到达图中所示的桶,并首先遍历其属于较新的run的指纹中的指纹(即,查找最新条目的版本)。对于run 1,我们得到否定,因为指纹不同。对于run 2,我们得到误报,导致储存器i/o浪费。对于run 3,我们得到真肯定,因此目标条目被返回给应用。
[0156]
每当lsm树的缓冲区将一批新的应用更新刷新到储存器时,chucky都会为该批中的每个键(包括墓碑)添加ff条目。例如,考虑图3中的条目k1,对此在run 3中最初有一个版本。然后,作为run 1的一部分,该条目的新版本被刷新到储存器中。因此,chucky添加了新的ff条目来说明这个更新的版本。这导致了临时空间放大(sa),该临时空间放大稍后通过合并来解决,同时条目被带到内存以进行排序合并。
[0157]
该sa是适度的,因为lsm树的指数结构限制了每个条目的平均版本数(例如,对于分级合并或惰性分级合并,t/(t-1)《2)。事实上,bf表现出完全相同的内存sa,因为跨不同run的bf的条目的每个版本占用每个条目m位。
[0158]
对于合并运行时识别和丢弃的每个过时条目,chucky会从ff中移除相对应的条目。
[0159]
对于每一个其他条目,chucky将其run id更新为被创建的新run id。
[0160]
因此,chucky维护ff的run id,而不需要任何额外的储存器i/o。
[0161]
此外,chucky允许在合并操作中继承run id,以避免ff更新并节省内存i/o。它通过将lsm树的层级i处的第j最旧run的run id设置为(i-1)*k j来实现这一点。因此,run id的范围从1到a,其中(来自等式(1))a是run数量。实际上,这意味着条目的run id只在该条目被合并到新的层级时发生变化,而不是在给定条目在合并后保持在同一层级时发生变
化。
[0162]
例如,在图3中,当run 1、2和3合并成在层级3处的新的run时,新的run也被分配了run id 3。在合并操作期间,我们从ff中识别并移除条目k1的旧版本,并将条目k2和条目k1的新版本的run id更新为3。然而,我们保持条目k3的run id相同,因为新的run继承了旧的run 3的id。
[0163]
应用查询探测ff一次,而更新访问它均摊的l次(每次更新条目进入新层级时访问一次)。
[0164]
表3总结了这些属性。相对于表1中bf的内存i/o复杂度,chucky将查询成本降低到一个常数。此外,它降低了更贪婪的合并策略的更新成本,且从而分离查询和更新的内存i/o成本。通过这种方式,chucky在内存带宽方面主导了布隆过滤器。
[0165]
表3
[0166] 分级合并惰性分级合并分层合并应用查询o(l)o(1)o(1)应用更新o(l)o(l)o(l)
[0167]
chucky的调用复杂度
[0168]
表4
[0169] 分级合并惰性分级合并分层合并统一o(2-m
*l)o(2-m
*l*t)o(2-m
*l*t)最优o(2-m
)o(2-m
)o(2-m
*t)
[0170]
没有run id压缩的fpr界限
[0171]
正如我们前面看到的,ff桶中的二进制编码run id随着数据量而增长,从而从指纹中提取位并增加误报率。为了防止这个问题,我们现在详细探讨如何使用压缩来使run id尽可能小。
[0172]
run id是非常可压缩的,因为它们遵循近似几何概率分布。
[0173]
我们使用等式(5)将它公式化,等式(5)将pi表示为在lsm树的层级i的用户数据的分数。
[0174]
(5)
[0175]
id为j的run位于lsm树的层级[j/k]。因此,它的频率是层级概率p
[j/k]
(来自等式5)除以该层级的run数量a
[j/k]
(来自等式1)。
[0176]
因此,我们在等式(6)中将fj表示为第j个run id的频率。
[0177]
(6)
[0178]
对于较小层级的run,这些概率呈指数下降。因此,可以用较少的位来表示较大的run id,用较多的位来表示较小的run id。由于较小的run id的频率呈指数下降,用于表示run id的平均位数将保持较小。
[0179]
为了建立关于run id可以压缩多少的限制,我们导出了它们的香农熵,该香农熵代表了在给定概率分布内表示项目所需的平均位数的下限。
[0180]
我们在等式(7)中通过陈述不同run id概率上的熵的定义,分别代入等式(1)和
(5)的ai和pi,并进行简化来这样子进行。
[0181]
有趣的是,熵会收敛到常数,该常数与层级的数量无关,且因此不会随着数据量而增长。
[0182]
直觉是,较小层级的run id概率的指数下降超过了较小层级的run id需要更多的位来唯一表示的事实。
[0183]
(7)
[0184]
通过将等式(7)代入等式的run id长度d,我们获得了表4中的fpr界限。这些界限适用于每次查找检查的指纹数量为小常数的任何ff(即,实践中迄今为止的所有ff)。
[0185]
这些界限低于表2中关于最优bf的界限的事实再次证实了我们的方法;就fpr而言,具有压缩run id的ff能够匹配bf或者甚至改进bf。在下一节中,我们展示如何在实践中做到这一点。
[0186]
为了在实践中压缩run id,我们使用霍夫曼编码。霍夫曼编码器将run id及其概率(来自等式(6))作为输入。作为输出,它返回二进制代码来表示每个run id,且由此更频繁的run id被分配更短的代码。通过首先将可能性最小的run id连接起来作为子树,从run id创建二叉树来这样做。run id的最终代码长度对应于它在产生的树中的深度。
[0187]
图4示出了带有标记的run id的惰性分级合并lsm树(该树的参数是t=5,k=4,z=1),每个id都具有公式(6)中的相对应的频率。我们将这些run id及其频率输入霍夫曼编码器,以获得旁边显示的霍夫曼树。通过连接从根节点到给定run id的叶节点的路径上的树的边标签,给出了run的代码。例如,run id 4、8和9的代码分别是011011、010和1。
[0188]
使用霍夫曼编码,没有代码是另一个代码的前缀。该属性允许对输入位流进行唯一解码,方法是从根开始遍历霍夫曼树,直到到达一个叶,在给定的叶输出run id,且然后从根重新开始。例如,输入位流11001基于图4中的霍夫曼树被唯一解码成run id 9、9和7。该属性允许唯一地解码桶内的所有run id,而不需要定界符号。
[0189]
我们使用等式(8)中定义的平均代码长度(acl)来测量编码run id的大小,其中lj是分配给第j个run的代码长度。
[0190]
例如,这个等式为图4中的霍夫曼树计算了1.52位。这相对于二进制编码节省了62%,二进制编码需要四位来唯一地表示九个run id中的每一个。
[0191]
(8)
[0192]
信息理论中众所周知的是,霍夫曼编码的acl的上限是熵加1。增加1的直觉是每个代码长度被四舍五入(rounded up)到一个整数。我们将其表示为acl≥h 1,其中h是等式(7)中的熵。因此,我们期望这种情况下的acl收敛并变得独立于数据量,与等式(7)相同。
[0193]
我们在图5中通过增加图4中示例的层级数并示出了霍夫曼acl来验证这一点,该acl确实收敛了。直觉是,虽然较小层级处的run被分配了较长的代码,但这些代码的频率却呈指数级下降。相比之下,二进制编码需要更多的位来唯一地表示所有的run id。因此,霍夫曼编码允许更好地缩减内存占用。
[0194]
在一次编码一个符号的压缩方法中,已知霍夫曼编码是最优的,因为它最小化了acl。然而,精确的acl很难进行分析,因为霍夫曼树结构很难从一开始就预测。相反,我们可
以通过假设一种不太通用的编码方法,并观察到霍夫曼acl将至少同样短,来推导出关于等式(8)比以前更严格的上限。例如,我们可以使用(1)长度为l-i 1位的一元编码前缀来表示层级i,然后使用(2)长度约为log2(ai)的截断二进制编码后缀来唯一地表示在层级i处的每个a
i run,来表示每个run id。这实际上是golomb编码,其也适用于我们的问题,且更容易进行分析。
[0195]
然而,我们关注霍夫曼编码,因为它允许一次编码多个符号。我们暂时利用这种能力。我们在等式(9)中将这种编码的平均长度导出为acl
ub
,并在图5中将其示出为霍夫曼acl的合理的严格上限。
[0196]
(9) [0197]
图5进一步绘制了等式(7)中run id频率分布的熵。
[0198]
如图所示,霍夫曼acl和熵之间有一个差距(gap)。事实上,在图6中,显示了当增加lsm树的大小比t时,acl和熵之间的差距会增大。(该图是为分级合并的lsm树绘制的(即k=1,z=1))。原因是,到目前为止,一直一次编码一个run id,这意味着每个run id至少需要一个位来用代码表示。因此,acl不能低于每个run id一位。另一方面,随着概率分布变得更加偏斜,熵继续朝向零下降,因为分布中的信息内容(即意想不到的(surprise)的量)减少。信息理论中克服这一限制的一般方法是一次编码多个符号,正如现在继续探索的那样。
[0199]
ff以适度的fpr牺牲实现高负载系数的一种常见技术是每个桶存储多个指纹。
[0200]
现在显示如何利用这种ff设计决策来对桶内的所有run id进行集体编码,以进一步推动压缩。
[0201]
图7给出了一个示例,其说明如何对分级合并lsm树(具有两级,且大小比t为10)一次编码两个run id的排列(permutations)。排列的概率是等式(6)中其组成run id的概率的乘积。例如,排列21和22的概率分别是10/11*1/11和(10/11)^2。通过将大小为2的所有可能的run id排列及其概率输入到霍夫曼编码器中,获得了标记为perms的霍夫曼树,图7中的acl为0.63。
[0202]
这是对一次编码一个run id的改进。这种改进的直觉是,可以用比排列中符号数量更少的位来表示最常见的排列。
[0203]
图6显示,随着排列大小的增加,生成的霍夫曼树的acl接近熵,在排列大小为4或更大的情况下,该熵非常近似acl。
[0204]
在图7的示例中,相同的run id有两种排列:21和12。对于遇到任一排列的查询,相同的查找过程随之而来:检查run 1的键(即,首先是指纹,且如果是肯定的,则也在储存器中),并且如果没有找到,继续检查run 2。两种排列触发相同的过程的事实,意味着排列编码了关于顺序的冗余信息。相反,可以对run id的组合进行编码,如图7所示,其中组合12取代了之前的两个排列。
[0205]
由于与a^s不同,组合比排列少需要更少的位来表示它们,且因此acl可能会比之前下降得更低。
[0206]
为了用编码的组合来降低acl的界限,通过从原始熵表达式h(来自等式(7))中减去所有关于顺序的信息,在等式10中导出了新的熵表达式h
组合
。该顺序信息相当于log2(s!)
位来排列s run id,同时对于任何重复j次的run id,二项式地忽略(binomially discounting)log2(j!)位。由于组合是多项分布的,推导相同表达式的另一种方法是通过多项分布的熵函数。除以s,将表达式规范化为每个条目,而不是每个桶。
[0207]
(10) [0208]
当增加集体编码的run id的数量时,图8将h
组合
与h进行了比较。(这个示例使用了分级合并的lsm树,其中t=10,k=1,z=1以及l=6)。观察到,集体编码的run id越多,h
组合
下降得越多,因为相对于h它消除了更多关于顺序的冗余信息。
[0209]
为了在实践中使用编码组合,必须根据它们的run id对每个桶中的指纹进行排序,以便能够识别哪个指纹对应于哪个run id。为了进行实际编码,将所有可能的组合及其概率馈送到霍夫曼编码器中。我们使用多项式分布在等式(11)中表示组合c的概率c
概率
,其中c(j)表示组合内第j个run id的出现次数。
[0210]
例如,对于图7中的组合12,我们有s=2,c(1)=1和c(2)=1。因此,概率是2!*(1/11)*(10/11)=10/121。
[0211]
(11) [0212]
对于组合,acl是∑
c∈c
ic*c
概率
/s,其中c是所有组合的集合,以及ic是组合c的代码长度(我们除以s来表示每个run id而不是每个桶的acl)。我们观察到组合acl主导着图8中的排列acl,并且随着我们增加集体编码的run id的数量,它随着组合熵而收敛。
[0213]
在本文的其余部分,我们继续使用编码组合,因为它们实现了最优压缩。
[0214]
将代码与指纹对齐
[0215]
由于压缩,run id码长度可变,因此将它们与ff桶中的指纹对齐成为一个挑战。我们在图9的(a)中通过将两个条目的一个run id组合码与十六位ff桶中的两个五位指纹(fp)对齐来说明这一点。这个示例基于图4中的lsm树实例,不同之处在于现在编码run id组合,而不是单独编码每个run id。图中的术语l
x,y
是分配给具有一致run id x和y的桶的码长度。我们观察到,虽然一些代码和指纹在桶内完全对齐(行i),但其他会出现未溢出(underflow)(行ii)和溢出(overflow)(行iii和iv)。
[0216]
由于所具有的代码较短,因此具有频繁run id的桶内会发生未溢出。它们是不受欢迎的,因为它们浪费了原本可以用于增加指纹大小的位。另一方面,由于所具有的代码较长,因此具有较不频繁run id的桶内会发生溢出。它们是不受欢迎的,因为它们需要将剩余的桶内容存储在其他地方,从而增加了内存开销。
[0217]
我们在图10中用标记为统一指纹的曲线说明了溢出和未溢出之间的竞争。该图是为具有配置t=5、k=4、z=1、l=6的惰性分级合并lsm树和具有包含4个条目的32位桶的ff绘制的。该图改变了溢出的ff桶的最大允许分数,并测量了最大可能的对应指纹大小。
[0218]
如图所示,在指纹大小统一的情况下,指纹大小必须迅速减小,以保证溢出更少。
[0219]
为了解决这个问题,我们了解到run id组合分布(在等式(11)中)是重尾的(heavy-tailed),因为底层run id分布是近似几何的。因此,我们的方法是通过调整代码和指纹的大小同时允许沿着分布的重尾的所有其他组合溢出,来保证代码和指纹在最有可能的组合中完美地对齐。使用以下两种互补技术分两步实现这一目标:可延展指纹(mf)和流
体对齐编码(fac)。
[0220]
可延展指纹(mf)—为了便于对齐,mf允许来自不同lsm树层级的条目具有不同的指纹大小。
[0221]
然而,即使它通过ff的冲突解决方法跨桶交换,单个条目的指纹长度也保持不变。这意味着没有指纹位需要被动态削减或添加。一旦条目被移动到新的层级,如果需要的话,mf会给它分配新的指纹大小,同时将它放入内存进行排序合并。
[0222]
mf出现的问题是如何为每个层级选择指纹长度,以在指纹大小和溢出之间取得最优平衡。我们把这构造为整数规划(integer programming)问题。其中fpi表示在层级i处条目的指纹的(正整数)长度。目标是最大化平均指纹大小,如等式(12)所示:
[0223]
(12) [0224]
使用额外的参数nov来约束这一问题,以得到想到保证的非溢出桶的分数(理想情况下至少为0.9999)。使用这个参数将c
freq
定义为c的子集,它只包含c中最有可能的run id组合,该最有可能的run id组合的累积概率刚好高于nov。
[0225]
我们在等式13中将其添加到问题作为约束,该约束要求对c
freq
中的所有c,代码长度(表示为lc)加上累积指纹长度(表示为c
fp
)不超过桶中的位数b:
[0226]
(13) [0227]
虽然整数规划是np完全的,且因此很难进行全局优化,但我们运用该问题的特定结构,以及算法1中所示的有效爬山(hill-climbing)方法。该算法将所有指纹大小初始化为零。然后,它尽可能增加较大层级的指纹大小,当违反等式13中的溢出约束时,移动到下一个较小层级。首先延长更大层级指纹的理由是它们的条目更频繁。以这种方式,算法遵循最陡的上升(steepest ascent)。图9显示了mf在消除一些溢出(行iii)的同时如何降低未溢出(行ii)的严重性。因此,如图10所示,它能够更好地平衡溢出和平均指纹大小。
[0228]
流体对齐编码(fac)—图9的(b)示出了,即使使用mf,未溢出和溢出仍然会发生(分别见,行ii和行iv)。为了进一步减轻它们,引入了fac。fac利用了信息理论中众所周知的折衷,即前缀码中设置的一些代码越小,其他代码必须越长,所有代码才能保持唯一可解码的。
[0229]
这种折衷体现在kraft-mcmillan不等式中,该不等式指出,对于给定的一组代码长度l,如果1≥∑
l∈l 2-l
,所有的代码都是唯一可解码的。直觉是,代码长度是从计为1的预算设定的,且较小的代码消耗了该预算的较高比例。
[0230]
为了利用这种折衷,fac为非常频繁的桶组合分配占用未溢出位的较长代码。因此,关于所有其他桶组合的代码可以变得更短。这在不太频繁的桶组合中创造了更多空间,可以利用这些空间来减少溢出,并增加较小层级的指纹大小。在图9的(c)中说明了这个想法。
[0231]
行ii中的组合是系统中最频繁的,现在被分配了比以前更长的代码。这允许减小所有其他组合的代码长度,进而允许为层级1和2的条目设置更长的指纹,并消除行iv中的桶溢出。
[0232]
在mf的顶部实现fac,如下所示。首先,用新的约束替换先前的溢出约束(等式(13)),如等式\refeq:constraint3所示。用kraft-mcmillan不等式表示,它确保指纹大小
保持足够短,使得仍然有可能为c
freq
中的所有组合构造具有唯一可解码代码的非溢出桶。此外,还确保不在c
freq
中的所有其他桶组合都可以使用最多相当于桶b大小的唯一代码进行唯一标识。
[0233]
(14) [0234]
注意,等式14不依赖于预先已知霍夫曼码(即,像等式(13)那样)。因此,可以在使用算法1找到指纹长度之后而不是之前运行霍夫曼编码器。
[0235]
第三,仅在c
freq
中的组合上运行霍夫曼编码器,同时将组合c的频率输入设置为而不是像以前那样使用(在等式(11)中的)其多项式概率。
[0236]
这使得霍夫曼编码器生成的代码正好填满剩余的位b-c
fp
。第四,对于不在c
freq
中的所有组合,设置大小为b位的统一大小的二进制代码,它由霍夫曼树中的公共前缀和唯一后缀组成。以这种方式,可以唯一地识别和解码两组中的所有代码,其由不在霍夫曼树中的公共前缀和唯一后缀组成。
[0237]
图10示出了当一起应用时,mf和fac消除了在溢出和指纹大小之间的竞争。事实上,他们将平均指纹大小保持在理论最大值的接近值(在图中的半个位以内),该值是通过从每个条目的位的数量m减去(在等式(10)中的)组合熵得到的。我们针对本文的其他部分默认使用mf和fac。
[0238]
算法1的运行时间是o(l*m*|c|),其中l*m是迭代次数,|c|是评估等式(14)中约束的成本。另外,霍夫曼编码器的时间复杂度是o(|c|*log2(|c|))。该工作流很少被调用(即,仅当lsm树的层级数量改变时被调用),并且它可以离线执行。因此,它的运行时间是实际的(图10中的每个点都要花费几分之一秒来生成)。
[0239]
chucky的fpr很难精确分析,因为指纹具有各种从一开始就不知道的大小。
[0240]
相反,我们给出了一个保守近似值,从而仍然允许对系统行为进行推理。首先,我们观察到,对于fac,平均代码长度总是每条目至少一位,因此我们使用等式(9)中的上限acl_ub来稍微高估它。因此,我们将平均指纹大小近似为m-acl_ub,从而将单个指纹的fpr近似为2^-(m-acl
ub
)。我们将该表达式乘以因子q,该因子表示每次探测由底层ff搜索的指纹的平均数量(例如,对于每桶四个条目的布谷鸟过滤器,q约为8)。因此,我们得到等式(15),对于等式(15),解释是对非存在键(non-existing key)的查询的误报的预期数量。实际上,实际的fpr倾向于偏离这个表达式最多2倍。
[0241]
(15) [0242]
我们现在讨论在应用读取操作时解码run id以及在写入操作时对它们重新编码所需的数据结构。具体来说,我们示出了如何防止这些结构成为瓶颈。
[0243]
由于霍夫曼代码(huffman code)是可变长度的,因此我们通常无法在恒定时间内对它们进行解码(例如,使用查找表),因为我们从一开始就不知道所讨论的给定代码有多长。因此,对霍夫曼代码的解码通常是通过基于所讨论的代码从根到给定叶遍历霍夫曼树一次一位地完成的。一个可能的问题在于,如果霍夫曼树很大,则遍历它可能需要每个访问节点多达一个内存i/o。
[0258]
但是,如果两个桶都满了,则这些桶中一个桶的一些指纹y将被逐出以清除空间。指纹y使用等式\refeq:cuckooc交换到其替代桶中,该等式不依赖于原始键(通过使用异或运算符),而仅依赖于指纹和当前包含y的桶i。
[0259]
(18) [0260]
交换过程递归地继续,直到找到对于所有指纹的空闲桶槽,或者直到达到交换阈值,此时原始插入失败。查询最多需要两个内存i/o,因为每个条目被映射到两个可能的桶。在本文中,我们采用布谷鸟过滤器,其中每个桶有四个槽。众所周知,这种调整能够以高概率达到95%的容量,而不会导致插入失败,并且每次插入只有1-2次均摊交换(amortized swap)。
[0261]
为了在cf的顶部实现chucky,我们在每个cf桶的开始处放置一个组合代码,后跟可变大小的指纹。我们使用保留的全零指纹结合有最频繁的run id来表示空指纹槽,以最小化相应的组合代码长度。另外,我们进行以下调整。
[0262]
由于布谷鸟过滤器依赖异或运算符来定位条目的替代桶,因此桶的数量必须是2的幂。这可能会浪费高达50%的分配内存,特别是当lsm树的容量刚好超过2的幂时。为了解决这个问题,借鉴真空过滤器的思想,将一个cf分成多个独立的cf,每个cf是2的幂,但是cf的总数是灵活的。以此方式,通过改变cf的数量,容量变得可调节,并且使用散列模运算将每个键映射到组成cf之一。我们将每个cf设置为8mb。
[0263]
当chucky达到容量时,需要调整其大小以容纳新数据。然而,cf不能有效地重新调整大小。最简单的方法是在chucky达到容量时从头开始重建它。然而,这种方法强制对数据集进行昂贵的扫描,以将所有条目重新插入到chucky的新实例中。作为替代,我们利用这样一个事实,即进入lsm树的最大层级的合并操作会覆盖整个数据集。利用这个机会也建立了chucky的一个新实例,从而避免了额外扫描的需要。将chucky的新实例的大小设置为比当前数据量大了倍,以适应下一次完全合并之前的数据增长,并始终在所有cf中保持5%的备用容量,从而防止插入失败。
[0264]
由于chucky为不同层级的条目分配不同的指纹大小,因此出现了一个问题,cf可以将不同层级的条目的不同版本映射到两个以上的cf桶。
[0265]
通过确保所有指纹至少包含x位来解决这个问题,并根据条目的前x位调整cf以确定条目的替代桶。这迫使同一条目的所有版本驻留在同一对cf桶中。根据布谷鸟过滤器文献,我们将最小指纹大小设置为5位,以确保一个条目的两个桶足够独立,从而实现95%的负载系数。
[0266]
由于cf将来自不同lsm树的run的同一条目的多个版本映射到同一对cf桶中,因此如果给定条目的版本超过八个,就会发生桶溢出。一些ff可以使用嵌入式指纹计数器(例如计数商过滤器(counting quotient filter))用现成方法(out-of-the-box)解决这个问题。然而,对于我们的cf设计,我们使用附加散列表(aht)来解决这个问题,该散列表从桶id映射到溢出条目。在插入繁重的工作负载的情况下,aht一直是空的。即使在更新繁重的工作负载的情况下,aht仍然很小,这是由于lsm树的设计限制了空间放大,从而限制了每个条目的平均版本数(例如,使用分级合并或惰性分级合并时,最多)。检查aht在查询或
更新过程中遇到的每个满的ff桶,从而向它们添加最多o(1)个额外的内存访问。
[0267]
对于每个run,都将其条目的指纹保存在储存器中。在恢复期间,只从储存器中读取指纹,从而避免对数据进行全面扫描。以每个条目实际上恒定的均摊内存i/o成本将每个指纹连同其run id插入到一个全新的cf系列。以此方式,恢复在储存器和内存i/o方面都是高效的。
[0268]
评估
[0269]
现在给出一个表达式,一般来说,它近似地表示由于ff引起的预期i/o。
[0270]
所使用的机器具有32gb ddr内存和四个2.7ghz内核,带有运行ubuntu 18.04 lts的8mb l3缓存,并通过pcie连接到512gb ssd。
[0271]
使用我们自己的lsm树实现,其基于陀思妥耶夫斯基(dostoevsky)进行设计,并且正准备将其用于商业用途。添加了具有统一误报率(fpr)的阻塞缓存和非阻塞bf作为基线,以分别表示rocksdb和cassandra中的设计决策。
[0272]
还支持最优fpr。
[0273]
默认设置包括一个具有1mb缓冲区的惰性分级合并lsm树,大小比为5,且六个层级的数据量约为16gb。每个条目都是64b。具有1gb块缓存,并且数据结构块大小为4kb。chucky使用每条目10位和5%的预留空间(over-provisioned space)。因此,所有bf基线都被分配了多1/0.95倍的内存,以均衡基线之间的内存。
[0274]
图中的每一点都是三次实验的平均值。
[0275]
当最频繁访问的数据在块缓存中时,使用统一的工作负载分布来表示最坏情况的性能,并且使用zipfian分布来创建偏斜并阐明性能性质。
[0276]
图12a比较了随着数据增长,在具有统一工作负载的情况下,使用chucky相对于阻塞的bf和未阻塞的bf(均具有最优fpr)的读取/写入延迟。写入延迟是通过将过滤器维护所花费的总时间除以应用发出的写入次数来测量的。读取延迟刚好在完全合并操作之前测量(当系统中存在最多run时),以突出最差情况的性能。
[0277]
未阻塞的bf表现出最快增长的延迟,因为它们在不断增多的过滤器中每个过滤器需要多个内存i/o。此后,我们在评估中放弃未阻塞的bf,因为它们是不具竞争性的。
[0278]
在阻塞的bf的情况下,读取/写入延迟增长更慢,因为每次读取或写入它们最多需要一个内存i/o。
[0279]
chucky的写入延迟也随着数据的增长而缓慢增长,因为存在在其上需要更新run id的更多层级。
[0280]
至关重要的是,我们观察到chucky是唯一能够保持读取延迟随数据量稳定的基线,因为每次读取都需要恒定数量的内存i/o。
[0281]
图12b堆叠了在不同lsm树变体的情况下chucky相对于阻塞的bf的读取和写入延迟。
[0282]
chucky全面提供了更好的成本平衡,主要是因为它较低的读取延迟。然而,chucky也提高了对于分层级的lsm树设计的写入成本。原因在于,利用分级合并时,合并是贪婪的(greedy),因此bf被快速重构,导致每层级每个条目有多个bf插入。相比之下,chucky总是要求每层级每个条目只有一次更新。总的来说,chucky不仅改善了过滤器读取/写入成本平衡,而且使它们独立于底层的lsm树变体。这使得系统更容易推理和调整。
[0283]
图12c将对于具有压缩和未压缩的run id的chucky的fpr与具有统一和最优空间分配的阻塞的bf的fpr进行了比较。随着数据量的增加,具有未压缩的run id的chucky的fpr会增加,因为run id会增长并从指纹中窃取位。
[0284]
利用统一bf,fpr也随着数据量的增长而增长,因为有更多的过滤器可以发生误报。相比之下,在最优bf下,较小的层级被分配指数级降低的fpr,因此fpr的总和收敛到与层级数无关的常数。类似地,随着数据的增长,chucky的fpr保持恒定,因为平均run id代码长度收敛,从而允许大多数指纹保持较大。该图还包括等式(15)中的chucky的fpr模型,以表明它给出了fpr在实践中的合理近似。
[0285]
图12d示出了chucky要求每个条目至少8位才能工作(即,代码和最小指纹大小)。然而,由于每个条目11位及以上,chucky提供了比所有bf变体更好的内存/fpr折衷。原因在于,众所周知的,bf表现出次优的空间使用,这有效地将内存预算减少了ln(2)倍。因此,chucky相对于内存更好地缩减fpr。
[0286]
为了允许chucky在每个条目少于8位的情况下操作,同时还保持fpr较低,可以在lsm树的最大层级处使用bf,并且对于所有较小层级使用ff。由于篇幅限制,我们暂时不考虑这样的设计。
[0287]
图12f和图12g分别测量了具有统一和zipfian(参数s=1)工作负载的端到端读取延迟。读取延迟分为三个部分:(1)储存器i/o,(2)跨越栅栏指针、缓冲区和块缓存的内存中搜索,以及(3)过滤器搜索。在(f)部分中,相关数据通常存储在储存器中,因此储存器i/o主导读取成本。然而,由于我们的ssd很快,因此bf探测仍然会带来明显的延迟开销,chucky能够消除这种开销。另一方面,在(g)部分中,工作负载是偏斜的,这意味着目标数据通常位于块缓存中。在这种情况下,bf成为瓶颈,因为它们在能够识别缓存中的相关块之前必须先被搜索。chucky缓解了这一瓶颈,从而显著改善了读取延迟。
[0288]
图12h示出了当我们增加对于由95%zipfian读取和5%zipfian写入组成的工作负载的数据量时,吞吐量是如何缩减的(以workloadb为模型)。bf基线不能很好地缩减,因为它们在越来越多的bf上发布内存i/o。具有未压缩的run id的chucky也随着其fpr的增长而表现出不断恶化的性能,并导致更多的储存器i/o。具有压缩的run id的chucky也表现出不断恶化的性能,主要是因为在栅栏指针上的二元搜索的成本不断增加。然而,chucky提供了比所有基线更好的数据吞吐量,因为它同时缩减过滤器的fpr和内存i/o。
[0289]
图13示出了方法300的示例。
[0290]
方法300用于管理键值(kv)对的日志结构合并(lsm)树。lsm树存储在非易失性存储器中,该方法可以包括:
[0291]
方法300可以从步骤310开始。
[0292]
步骤310可以包括生成或接收指示当前kv对的当前指纹。当前kv对被包括在当前run中。
[0293]
步骤310之后可以是步骤320,其将当前run从缓冲区写入lsm树内的当前run位置,当前run可以包括当前kv对。当前run可以被排序。
[0294]
步骤310和320之后可以是步骤330,其通过向管理数据结构(mds)添加在当前kv对、当前指纹和当前run标识符之间的映射来执行mds的run写入更新(run writing updat)。
[0295]
mds的run写入更新反映了步骤310的执行。
[0296]
步骤330可以在不检查lsm树内当前kv对的先前版本的存在的情况下执行。
[0297]
步骤330可以被执行,而不管lsm树内当前kv对的先前版本是存在还是不存在。
[0298]
步骤330之后可以是步骤310和/或320。
[0299]
方法300可以包括通过合并lsm树的至少一些run来更新lsm树的步骤340。
[0300]
步骤340可以包括将lsm树的可包括第一kv对的第一run与lsm树的可包括第二kv对的第二run合并。
[0301]
步骤340可以包括将第二kv对添加到第一run,并且其中合并更新的执行可以包括在保持与第一kv对相关联的run标识符的同时更新与第二kv对相关联的run标识符。
[0302]
步骤340可以包括将第一kv对和第二kv对写入lsm树的第三run,其中合并更新的执行可以包括更新与第一kv对以及与第二kv对相关联的run标识符。
[0303]
步骤340可以包括当kv对的较新版本可以包括表示删除命令的值时,删除该kv对的先前版本。
[0304]
步骤340可以包括合并属于lsm树的不同层级的至少两个run。
[0305]
步骤340可以包括合并属于lsm树的同一层级的至少两个run。
[0306]
步骤340之后可以是步骤350,步骤350执行mds的合并更新以表示合并。
[0307]
步骤350之后可以是步骤340。
[0308]
方法300可以包括每当run被写入非易失性存储器时,触发lsm树的一个或更多个层级的run的合并。
[0309]
方法300可以包括每当lsm树的一个或更多个层级达到满度水平(fullness level)时,触发所述一个或更多个层级的run的合并。
[0310]
合并可以根据任何方法执行,诸如分级合并、惰性分级合并和分层合并。
[0311]
mds可以包括多个桶,每个桶可以被配置为存储与两个或更多个kv对相关的元数据。
[0312]
方法300可以包括步骤360,步骤360接收用于访问存储在非易失性存储器中的所请求的kv对的请求。访问请求可以是读取所请求的kv对的请求。kv对被称为请求的kv对,因为它包含在请求中。
[0313]
步骤360之后可以是步骤370,步骤370使用所请求的kv对的键访问mds以获得相关run的位置。
[0314]
步骤370之后可以是步骤380,步骤380在相关run存在时检索相关run。应当注意,可以分配专用值(墓碑)来指示删除先前的kv对。当相关run包括具有这种专用值的键时,响应在于所请求的kv对在lsm树中不存在。
[0315]
步骤380之后可以是等待接收新请求,并且当接收到请求时跳到步骤360。
[0316]
图14示出了方法400的示例。
[0317]
方法400用于管理键值(kv)对的日志结构合并(lsm)树,该lsm树被存储在非易失性存储器中。
[0318]
方法400可以包括合并lsm树的run以提供合并run的步骤410。
[0319]
方法400可以包括向lsm树添加新run的步骤420,其中添加可以包括向非易失性存储器写入run。
[0320]
步骤410和/或步骤420之后可以是更新至少一个管理数据结构(mds)以反映合并和添加的步骤430。
[0321]
至少一个mds中的一个mds存储在lsm树的kv对的键、与lsm树的kv对相关联的指纹以及标识lsm树的run的压缩的run标识符之间的映射。
[0322]
压缩的run标识符可以使用可变长度代码(诸如但不限于霍夫曼代码)压缩。
[0323]
步骤430可以包括步骤440,步骤440通过应用可变长度编码来压缩run标识符以提供压缩的run标识符。
[0324]
lsm树可以包括第一层级和最后层级。第一层级小于最后层级。可以有一个因子t,其定义了一层级和前一层级之间的比率。
[0325]
步骤440可以包括分配比第一层级的run的压缩的run标识符更短的最后层级的run的压缩的run标识符。
[0326]
步骤430可以包括计算表示run标识符的组合的组合run标识符代码的步骤450。
[0327]
每个组合run标识符代码与形成由组合run标识符表示的组合的run标识符中每一个的指纹相关联。
[0328]
方法400可以包括确定lsm树的每层级的指纹中的每一个指纹的长度的步骤452。
[0329]
步骤454可以包括最大化层级的指纹长度乘以该层级在lsm树中的分数的乘积或者在lsm树的所有层级上的总和。
[0330]
步骤430可以包括步骤456,步骤456在mds的桶中存储多个集合,其中每个集合包括组合run标识符代码和形成由组合run标识符代码表示的组合的run标识符中每一个的指纹。这些可以提供对齐的集合。
[0331]
步骤430可以包括步骤458,步骤458将未被包括在桶中的溢出元数据存储在溢出mds中。
[0332]
步骤450可以包括计算表示run标识符的组合的压缩的组合run标识符代码。
[0333]
步骤450可以包括步骤451,步骤451对压缩的组合run标识符代码的最小长度施加约束。
[0334]
步骤450可以包括步骤453,步骤453对压缩的组合run标识符代码的最小长度施加约束,并为lsm树的每一层级确定指纹中每一个指纹的长度。
[0335]
方法400可以包括步骤460,步骤460接收用于访问存储在非易失性存储器中的所请求的kv对的请求。访问请求可以是读取所请求的kv对的请求。kv对被称为请求的kv对,因为它包含在请求中。
[0336]
步骤460之后可以是步骤470,步骤470使用所请求的kv对的键访问mds以获得相关run的位置。这可以包括获得压缩的run id并将其解压缩(将其解码)以提供未压缩的run id。
[0337]
步骤470之后可以是步骤480,步骤480在相关run存在时检索相关run。应当注意,可以分配专用值(墓碑)来指示删除先前的kv对。当相关run包括具有这种专用值的键时,响应在于所请求的kv对在lsm树中不存在。
[0338]
步骤480之后可以是等待接收新请求,并且当接收到请求时跳到步骤460。
[0339]
图15示出了缓冲区10、ssd 30、第一mds 50和用于控制run的写入、维护第一mds等的管理单元100。管理单元可以是控制器、处理器,可以由控制器和/或处理器等托管。
[0340]
假设生成许多run并将其发送到ssd 30。图15示出了第n个run的生成和储存,n是正整数,其可以表示第n个时间点。
[0341]
缓冲区10存储缓冲的内容12。
[0342]
当缓冲区10满了(或发生任何其他触发事件)时,当前run 20(n)被发送到ssd 30。当前run 20(n)包括排序的缓冲内容,该缓冲内容包括具有当前键的当前kv对。
[0343]
ssd存储ssd内容32。它包括lsm树40,其包括i个层级42(1)-42(i)。
[0344]
在第n个时间点,lsm树包括r个run,run 20(1)-20(r)。r是正整数。r的值可以随着时间变化。
[0345]
首先,mds 50存储键、指纹和run id之间的映射52。
[0346]
一旦当前run被写入ssd,则通过添加当前条目54来更新第一mds。第一mds 50已经(在第n个时间点)存储了先前条目56,每个run的先前键一个条目(反映lsm树的当前状态)。
[0347]
图16示出了合并操作。
[0348]
在ssd内容32中,lsm树的选定层级(或选定层级的一部分)被发送到易失性存储器,发生合并操作,其中选定层级42(i)的run被合并以提供修改的层级42’(i)。
[0349]
修改的层级可以替换选定层级。
[0350]
合并可以在多个层级的run之间执行。
[0351]
修改可以按run的一部分(或一个层级的一部分)接另一部分地执行。
[0352]
修改之后是更新52第一mds 50。
[0353]
图17示出了包括多个桶52(1)-52(s)的第一mds 50,s是正整数。
[0354]
每个桶可以包括指纹和run id(runid)的一个或更多个集合,例如,参见第一桶的指纹fp 53(1,1)、run id 54(1,1)、指纹fp 53(1,2)和run id 54(1,2)。再举另一个例子,参见例如指纹fp 53(s,1),run id 54(s,1),指纹fp 53(s,2)和run id 54(s,2)。
[0355]
每个桶的集合数可能不同于两个。
[0356]
图18示出了包括多个桶52(1)-52(s)的第一mds 50,s是正整数。
[0357]
每个桶可以包括指纹和压缩的run id(c_runid)的一个或更多个集合,参见例如第一桶的指纹fp 53(1,1)、压缩的run id 55(1,1)、指纹fp 53(1,2)和压缩的run id 55(1,2)。再举另一个例子,参见例如指纹fp 53(s,1),压缩的run id 55(s,1),指纹fp 53(s,2)和压缩的run id 55(s,2)。
[0358]
图19示出了包括多个桶52(1)-52(s)的第一mds 50,s是正整数。
[0359]
每个桶可以包括指纹和一个压缩的组合run id(cc_runid)的一个或更多个集合。压缩的组合run标识符表示run标识符的组合。每个压缩的组合run标识符与形成由组合run标识符表示的组合的run标识符中每一个的指纹相关联。压缩的组合run标识符和这些指纹形成一个集合。
[0360]
每个桶可以存储多个集合。例如,参见第一桶52(1),其存储(a)包括指纹fp 53(1,1)和53’(1,1)以及压缩的组合run id 56(1,1)的第一集合,以及(b)包括指纹fp 53(1,2)和53’(1,2)以及压缩的组合run id 56(1,2)的第二集合。
[0361]
图20示出了集合的未溢出和溢出。集合可以包括指纹fp 53(1,1)和53’(1,1)以及压缩的组合run id 56(1,1)。图20还示出了用于对齐目的为每个集合分配的固定大小。
[0362]
图20还示出了使用可延展指纹的示例(步骤61、62和63),并且还示出了可延展指
纹和流体对齐编码的组合(步骤61、64和65)。
[0363]
图21和图22示出了管理数据结构及其内容的各种示例。
[0364]
第一mdr 50存储在键、指纹和压缩的run标识符之间的映射52’。
[0365]
第一mdr 50存储在键、指纹和压缩的组合run标识符之间的映射52”。
[0366]
第一mdr 50和第二mdr 70的组合(例如解码表)。第一mdr 50可以存储在键、指纹和压缩的组合run标识符之间的映射52”,但是仅用于不超过预定大小的压缩的组合run标识符。第二mdr存储在键、指纹和组合run标识符之间的映射,但仅用于超出预定大小的组合run标识符(以压缩形式)。
[0367]
第一mdr 50和溢出数据结构72的组合。第一mdr 50可以存储在键、指纹和压缩的组合run标识符之间的映射52”,但是可能导致桶溢出的任何内容都可以存储在第一溢出数据结构72中。
[0368]
图22还示出了记录表80,该记录表80映射应该由单个压缩的组合run id和它们的压缩的组合run id(字段84(x))表示的单独run id(字段82(x))。索引x的范围在1和x之间,x是记录表80上的条目数。x可以随着时间变化。记录表接收确定压缩的组合run id的请求,并输出选定的cc_runid。
[0369]
除了存储映射52”的第一mdr之外,还提供了记录表80。
[0370]
虽然本发明的前述书面描述使得普通技术人员之一能够做出和使用目前可以被认为是其最优模式的事物,但是这些普通技术人员将理解和意识到本文的特定实施例、方法和示例的变体、组合和等同物的存在。因此,本发明不应受上述实施例、方法和示例的限制,而是受如所要求保护的本发明的范围和精神内的所有实施例和方法的限制。
[0371]
在前述说明书中,已经参考本发明的实施例的具体示例描述了本发明。然而,显然,在不脱离如所附权利要求中阐述的本发明的更广泛的精神和范围的情况下,可以在其中进行各种修改和改变。
[0372]
本领域技术人员将认识到,逻辑块之间的边界仅仅是说明性的,并且可选实施例可以合并逻辑块或电路元件,或者对各种逻辑块或电路元件施加功能的替代分解。因此,应当理解,本文所描述的体系结构仅仅是示例性的,并且实际上可以实施实现相同功能的许多其它体系结构。
[0373]
实现相同功能的部件的任何布置被有效地“关联”,使得实现期望的功能。因此,本文组合以实现特定功能的任何两个部件可以被看作彼此“相关联”,使得实现期望的功能,而与体系结构或中间部件无关。同样,这样关联的任何两个部件也可以被视为彼此“可操作地连接”或“可操作地耦合”以实现期望的功能。
[0374]
此外,本领域技术人员将认识到上述操作之间的边界仅是说明性的。多个操作可以组合成单个操作,单个操作可以分布在附加操作中,并且操作可以在时间上至少部分地重叠地执行。此外,替代实施例可以包括特定操作的多个实例,并且在各种其它实施例中可以改变操作的顺序。
[0375]
另外,例如,在一个实施例中,所示示例可以被实施为位于单个集成电路上或在同一设备内的电路。替代地,该示例可实施为以合适方式彼此互连的任何数目的单独集成电路或单独设备。
[0376]
然而,其它修改、变化和替代也是可能的。因此,说明书和附图被认为是说明性的
而不是限制性的。
[0377]
在权利要求中,置于括号之间的任何附图标记不应被解释为限制权利要求。词“包括”不排除除了在权利要求中列出的那些之外的其他元件或步骤的存在。此外,如本文所使用的术语“一(a)”或“一(an)”被定义为一个或多于一个。此外,在权利要求中的引导性短语(例如“至少一个”和“一个或更多个”)的使用不应被解释为暗示由不定冠词“一(a)”或“一(an)”引入另一个权利要求要素将包含这样引入的权利要求要素的任何特定权利要求限制到仅包含一个这样的要素的发明,即使同一权利要求包括引导性短语“一个或更多个”或“至少一个”和不定冠词(例如“一(a)”或“一(an)”)。同理适用于定冠词。除非另有说明,否则诸如“第一”和“第二”的术语用于任意地区分开这样的术语所描述的要素。因此,这些术语不一定旨在指示这样的要素的时间或其他优先级。某些度量在相互不同的权利要求中被叙述的不争事实并不指示这些度量的组合不能有利地被使用。
[0378]
虽然本文已经图示和描述了本发明的某些特征,但是本领域普通技术人员将想到许多修改、替换、改变和等同物。因此,应当理解,所附权利要求旨在覆盖落入本发明的真实精神内的所有这样的修改和改变。
[0379]
应当理解,为了清楚起见,在不同的实施例的上下文中所描述的本公开的实施例的各种特征也可以在单个实施例中以组合方式提供。相反,为简洁起见,在单个实施例的上下文中描述的本公开的实施例的各种特征也可以单独地或以任何合适的子组合被提供。
[0380]
本领域中的技术人员将认识到,本公开的实施例不被限制于上文所具体示出和描述的内容。相反,本公开的实施例的范围由所附权利要求及其等同物限定。
再多了解一些

本文用于企业家、创业者技术爱好者查询,结果仅供参考。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表

相关文献