IEEE TRANSACTIONS ON COMPUTERS, VOL. 73, NO. 1, JANUARY 2024
主要内容:设计了NVM存储层用于在LSM压缩过程中衔接内存和SSD/HDD
日志结构化合并树 (LSM 树) 是现代键值存储的核心数据存储引擎。随着云计算和数据中心的发展,它的采用速度迅速加快。尽管 LSM 树得到了广泛的应用,但它仍然面临严重的性能问题,例如写入停顿、写入放大和读取效率低下。
本文介绍了使用新兴的非易失性存储器 (NVM) 技术提高基于 LSM 树的键值存储性能的研究。我们的性能诊断表明,上述问题主要来自密集的热键值数据处理,而慢速存储设备又加剧了这一问题。为了解决热点瓶颈,**我们提出了一种基于混合存储的拆分日志结构化合并树,**利用 LSM 树固有的热数据和冷数据分离属性。我们的方法将频繁访问的小型高级别提升到快速 NVM 上,并将剩余的冷的大型低级别卸载到慢速设备中,从而有效地缩小了基于 DRAMdisk 的 LSM 树的性能差距。此外,我们通过提出各种新颖的技术来优化拆分 LSM 树的读写性能。我们构建了一个名为 SplitDB 的热点感知键值数据库并进行了大量实验。实验结果表明,与最先进的键值数据库相比,SplitDB 有效防止了写入停滞,实现了 6 倍的写入减少,并将读取吞吐量提高了 3.5 倍。
物联网和机器学习等现实应用中的数据呈指数级增长,对当今的云计算和数据中心供应商提出了重大挑战[1]。键值数据库,包括LevelDB [2]、RocksDB [3]和Cassandra [4],由于其易于理解的存储范例、快速检索和对非结构化数据的有效支持,为管理、存储和索引大量数据提供了有效的解决方案。键值数据库已成为当前云和数据中心服务提供商不可或缺的构建块,例如亚马逊[5]、Meta [3]和阿里巴巴[6]。
这些键值存储中的核心数据存储引擎是Log-Structured Merge树。LSM树是一种采用异地写入策略的写优化数据结构。LSM树将数据缓冲在内存组件中,并将它们惰性地写入磁盘。磁盘空间由组织为树结构的多个组件组成。**新版本数据会先到达高级别的磁盘组件,之后再迁移到低级别的组件,这个过程叫做 LSM 树压缩。由于旋转磁盘有利于顺序写入,因此内存组件刷新和数据压缩都充分利用了硬件的这一特性。**另一方面,数据读取会先搜索内存组件。如果没有找到数据,它会按照从上到下的顺序搜索磁盘上的每一级。这种 LSM 树设计主要会导致三个性能问题。
两个著名的问题降低LSM-Tree的写入效率
write stall(写暂停)
由于高级组件的大小限制,数据通过LSM-tree 压缩(compaction)将击倒低层级
高级组件(这里指的是内存/缓存一类的高价值硬件)会被清空来容纳从DRAM刷新来的新数据
然而,DRAM 比磁盘快几个数量级, 最终,由于低速压缩无法匹配DRAM数据刷新的速度,导致用户的写入容易因为高层空间的不足而卡住
write amplification
逐层压缩合数据维持了一个稳定的LSM 树结构,一个键值对必须跨层写入多次。层级合并策略会导致客户端写入时出现级联数据写入
同时,LSM tree 同样遭受低速读取。对于 DRAM 组件查找未命中,必须逐级搜索深层 LSM 树。逐级搜索涉及昂贵的 I/O 传输,用于定位磁盘组件和密钥比较。长读取路径成为基于 LSM 树的键值存储的一个严重缺点影响
作者 [10] 分析认为写停顿是由于 MemTable 刷新和数据压缩之间的性能干扰造成的,并提出了一种调度方法来减少这种干扰。
MatrixKV [8] 利用 NVM 加速 L0-L1 压缩,从而减轻数据压缩对写停顿的影响。许多研究工作致力于通过从多个方面改造 LSM 树压缩来减少写放大,例如消除值数据写入 [7]、避免低级 SSTable 重写 [11], [12] 和跳过相邻级别合并 [13]。
为了减少长 LSM 树读取延迟,先前的研究提出了高效的辅助索引结构 [9], [14], [15](我们也是提出一种辅助索引结构)。例如,ChameleonDB [14] 引入了 DRAM 内哈希表以避免长时间逐级 LSM 树查找。ElasticBF [9] 提出了热点感知布隆过滤器设计来提高热 SSTable 读取性能。
以前的文章提出了各种方法来独立解决这些问题。与它们不同,本文提出了一个整体解决方案来同时应对这三个挑战。我们的解决方案源于一个新发现:对于倾斜的工作负载,LSM 树性能问题主要是由缓慢、密集的热数据处理引起的。我们在这里简要描述这一发现。第二节详细介绍了它
LSM 树包含多个级别,形成一个层次结构来满足数据请求。实际应用程序表现出倾斜的数据访问模式 [1],[16]:一小部分热数据吸引了更高的关注。频繁的热数据更新会生成大量集中在高层的键值记录。我们在第二部分中的 YCSB 工作负载实验报告称,L0-L1 压缩中超过 54% 的数据写入涉及热键值记录(本方法实现的LSM-tree不涉及热数据,且数据不可变)。大量的热数据写入是写入放大的决定因素。此外,在慢速设备上运行的长而昂贵的高级热数据压缩无法及时清空高级空间,导致客户端写入意外停顿。最后,热数据比冷数据读取密集得多。但是,压缩将热数据分散到多个树层上。查找分散的热数据会带来不可预测的长延迟,从而降低整体读取性能。
基于这些发现,本文设计并实现了一种新型热点感知键值数据库 SplitDB。它将新兴的 NVM 存储技术无缝融入 LSM 树设计中,以解决与热点相关的挑战,从而有效提高现代键值存储的性能。
非易失性存储器Non-volatile memories(如英特尔傲腾 DC 持久内存)提供接近 DRAM 的访问延迟、优于 SSD 的速度、类似磁盘的数据持久性和字节寻址能力 [17]。这些设备特性使 NVM 成为基于 DRAM-SSD/HDD 层次结构的中间层。此外,LSM 树原生支持热数据和冷数据分离。高层密集填充了大量更新的热键值记录,冷数据逐渐减少。利用这种特性,我们设计了一种基于混合存储的新型拆分 LSM 树。它将频繁访问的高层树提升到快速 NVM 上,并将较大的低层卸载到相对较慢的基于块的存储设备(本文中为 SSD)中。这种拆分 LSM 树架构保证了热数据处理事件(例如键值读取和数据压缩)可能发生在高速 NVM 中,从而缩小了内存存储层次结构中的性能差距。
我们充分利用 NVM 特性设计了分割 LSM 树。我们提出了多种有效的技术来优化分割 LSM 树的写入和读取路径。简而言之,==我们利用 NVM 字节寻址能力设计了一种新颖的基于链表的键值数据存储格式。==它有助于设计轻量级 DRAM 组件刷新和写入最佳 LSM 树压缩。此外,我们设计了一个级联跳表索引结构。它支持高效的跨级 LSM 树查找,从而减少了较长的 LSM 树搜索路径。最后,我们提出了有效的数据提升和降级机制来保留 NVM 中的热数据。
基于拆分的 LSM 树,我们构建了 SplitDB,一个基于异构 NVM-SSD 存储的高性能键值数据库。SplitDB 同时满足了热点感知、写入效率和崩溃容忍度。在实验中,我们将 SplitDB 与商用 LevelDB [2]、RocksDB [18]、NVM 优化的 NoveLSM [19] 和 MatrixKV [8] 进行了比较,并使用了一系列微基准测试和 YCSB 工作负载 [20]。实验结果表明,与最先进的基于 LSM 树的键值存储相比,SplitDB 最多可提高 3.5 倍/2.9 倍的读写吞吐量,最多可降低 6 倍的写入放大,并有效防止写入停顿。
总结:我们做了以下贡献
先驱者计划使用快速 NVM 来弥补商品键值存储中存储硬件的缓慢问题。
两个代表性作品(NoveLSM [19] 和 MatrixKV [8])采用了不同的方法,如图 2 所示。NoveLSM 将 MemTable 和 ImmuTable 放在 NVM 中。它为键值写入提供了快速且持久的数据写入
MatrixKV 发现,较小且较慢的 L0 级别容易导致写入停顿和写入放大问题。因此,MatrixKV 配置了一个较大的 L0 级别并将其存储在 NVM 中。
与它们相反,SplitDB 将高 k 级别放在 NVM 中。我们的动机有两个方面。首先,像英特尔 DCPMM 这样的可扩展 NVM 设备提供了很大的容量(128-512GB)。它与 512GB 到 2TB 的商业 SSD 相当 [17]。NVM 有足够的空间来保留多个 LSM 树级别。其次,我们的动机实验表明,高级热数据 R/W 事件(例如数据压缩、MemTable 刷新和 SSTable 查找)会导致性能问题。仅仅提高 L0 性能不足以完全解决这些问题
由于现有的 LSM 树性能问题主要是由热数据处理速度慢引起的,我们的主要思想是利用超快的 NVM 来弥补慢速存储设备的弱点,从而提高 LSM 树热点性能。图 3 展示了所提出的拆分日志结构合并树。
LSM 树本身具有多级结构,可提供热/冷数据分离。我们利用这一固有属性来设计一种新型的拆分日志结构合并树,该树跨越 NVM 和 SSD 的混合存储。拆分 LSM 树由两棵子树组成。其频繁访问的高层(即 H-LSM 树)被上传到快速 NVM 上,而较低层(即 L-LSM 树)驻留在慢速 SSD 上。L-LSM 树驻留在 SSD 存储中。它的键值布局、数据压缩和其他设计方面直接借鉴了现有的基于 SSD 的键值数据库(本文中的 LevelDB [2])。此外,H-LSM 树保存在快速 NVM 中,以确保快速满足热数据请求。具体来说,我们从两个方面提出了多种技术来提高 H-LSM 树的性能:
(1)我们利用 NVM 硬件特性来广泛优化其键值存储布局以及数据读写路径(第 III-B 至 III-E 节)
(2)我们提出了有效的数据升级和降级机制,以将热数据在很大程度上保留在 NVM 内部(第 III-F 节)。
我们为 H-LSM 树设计了一种称为 NVTable 的新型数据存储布局(这个数据结构只存放在NVM中)。LevelDB 中的 SSTable 将键值对组织为有序线性数组。NVTable 利用 NVM 字节寻址能力将键值对组织在一个未排序的双向链表(称为 KV 列表)中,如图 4 所示。
NVTable 插入是一个简单的 KV 列表附加,没有任何额外的数据移动。这种基于列表的未排序 KV 数据存储格式对于写入最佳的 H-LSM 树压缩设计非常重要。(这里在第四节有重要应用)
使用双向列表是因为硬件结构的原因
NVTable 后续需要作为类日志结构来实现容错恢复,所以此处需要存储数据的内存位置
虽然 KV 列表允许快速写入数据,但未排序的键值对组织会降低数据查找性能。键值对查找需要遍历整个 KV 列表并访问每个 KV 列表元素。我们引入了一个偏移数组来解决这个缺点。具体而言,NVTable 中的键值对顺序是偏移数组索引,而相关的内存地址偏移是偏移数组元素。使用偏移数组,NVTable 查找可以执行二进制搜索以确定目标数据内存地址。然后,它使用内存地址来获取键值项。偏移量数组和其他 NVTable 元数据(如最大键)存储在连续的内存区域中,而 KV 对存储在外部。偏移量数组是在 ImmuTable Flush 期间构建的,稍后将对此进行描述。
除了 L0 级之外,其他每个 H-LSM 树级都有一个具有非重叠键范围的 NVTable 集合。L0 级的 NVTable 是通过从 DRAM 中刷新 ImmuTable 生成的。L0 NVTable 无需进一步的数据合并或排序。它们的键范围可能会重叠。
H-LSM 树利用级联 NVMskiplist 来管理和索引除 L0 之外的每个 H-LSM 树级别的 NVTable。
L0 中的 NVTable 被组织为按创建时间戳排序的有序双向链表。它保证在 L0 中搜索 NVTable 遵循严格的时间顺序。
我们选择 skiplist 作为我们的 NVTable 索引结构(此处才是NVM硬件中索引NVTable的数据结构),因为它具有可证明的无锁查找和对多版本数据管理的有效支持。我们的 skiplist 是一个混合 DRAM-NVM 索引。它将 skiplist 底层存储在耐用的 NVM 中。Skiplist 上层位于快速 DRAM 中。每个 NVTable 都由其最大键索引。H-LSM 树级中的键值对查找包括两个阶段。首先,它遍历 skiplist 以找到 NVTable,其次,它在 NVTable 中搜索目标对。
传统的 LSM 树经常因其冗长的多级查找而因其读取效率低下而受到批评 [9],[14]。LSM 树查找成本高昂。它包括从当前树级的大量 SSTable 中定位目标 SSTable 并在 SSTable 中搜索键值对。我们提出了级联跳跃列表设计来减少 H-LSM 树查找延迟。
我们首先简要介绍现有的跳跃列表查找(skiplist )过程,然后描述我们的跨层查找优化。
具体来说,跳跃列表查找从最高级别开始并遍历列表以查找键大于或等于目标键的第一个节点(为什么要找这一个点?)。如果有匹配的键,则查找结束。否则,它向下重复此过程,直到到达最底层。
为了减少跳跃列表搜索中的查找范围,我们引入了一个称为级联指针的跨层指针。由于不同级别的 NVTable 具有重叠的键范围,级联指针可充当从上层跳跃列表的节点 1 到下层跳跃列表的节点 2 的快捷方式。节点 1 的键必须小于节点 2 的键才能保证正确的查找结果(这里的级联指针是为了快速找到低层级和高层于该键值相近(低于)的节点)。此外,节点 1 的键应尽可能接近节点 2 的键以最小化搜索范围。传统的跳跃列表搜索总是从最左边的节点开始。使用级联指针,上层跳跃列表搜索可以帮助减少低层跳跃列表搜索中的查找范围并有利于后续查找。
如图 5 所示,
在第一次查找键时,线程在 Lk 处执行常规跳跃列表搜索。它在底层找到目标跳跃列表节点并搜索 NVTable。如果未命中,它将搜索 Lk+1 处的跳跃列表。到达目标节点并在 NVTable 中找到目标键后,它会在 Lk 处创建一个级联指针,指向 Lk+1 处的目标节点。在第二次查找中,线程不是从 Lk+1 处的最左边的节点遍历,而是使用级联指针直接跳转到中间跳跃列表节点。这个中间节点非常接近目标节点,剩下的 Lk+1 搜索从这个中间节点开始。它有效地减少了 Lk+1 处的查找范围。
每个跳跃列表都提供无锁插入/删除/查找,并使用风险指针 [27] 进行安全内存回收。此外,每个跳跃表节点都包含一个自旋锁和一个引用计数器。当线程将一个级联指针从节点1设置到节点2时,它首先获取这两个节点的两个锁,然后增加节点2的计数器。只有当节点2的计数器达到零时,才能回收节点2。由于级联指针很少被修改,因此这种基于锁的方法几乎没有性能成本。
在传统的 LSM 树设计中,键值对首先持久化在 WAL 文件中,然后插入到 MemTable 中。当 MemTable 切换到 ImmuTable 时,ImmuTable 中的键值对将刷新到持久 SSTable。为了减少 ImmuTable 刷新延迟,我们提出了无日志键值数据插入,并消除了 ImmuTable 刷新路径中昂贵的数据持久化操作。
这里要明白:
- 原型为什么要用日志键值数据插入
- 为什么此处可以轻量化
如图 6 所示,
在线程将新的键值对插入 MemTable 之前
它首先创建一个持久键值对并在步骤 ❶ 中附加一个 KV 列表节点。KV 列表是一个双向链表。在当前的硬件架构上,原子地修改两个不连续的八字节字是不可能的。我们只使用 cmpxchg 指令原子地设置一个八字节后向指针(这里介绍了为什么使用双向链表),并通过 clwb 指令和 sfence 指令将其持久化。前向指针将在稍后的 ImmuTable 刷新期间创建。对于并发写入,我们使用链接和持久技术 [28] 来实现正确的持久顺序。
在步骤 ❷ 中,线程将键值对插入易失性 MemTable。此放入操作完成,无需任何额外的日志写入。当 MemTable 已满时,它将更改为 ImmuTable。后台线程将 ImmuTable 刷新到存储设备。幸运的是,ImmuTable 中的键值数据在放入期间已持久保存在 NVTable 中(这里NVTable的功能代替了日志)。在步骤 ❸ 中,我们扫描 ImmuTable 跳表的底层。底层是已排序的键值对的链接列表。我们按顺序读取每一对,在 NVTable 中找到其内存地址,并创建偏移数组元素。构建偏移数组后,我们向后遍历 KV 列表以在步骤 ❹ 中创建前向指针。之后,生成一个完整的 NVTable,并且可以释放此 ImmuTable。
传统的 LSM 树压缩选择一个或多个牺牲 SSTable,将数据加载到 DRAM 中(数据合并在内存中执行),并在合并和排序键值记录后将新的 SSTable 写入磁盘。(合并数据)
压缩期间重复的数据写入是写入放大的根本原因。利用 NVTable 数据组织,我们设计了写入优化的 NVTable 压缩,称为列表压缩。(根据NVTable实现的写入优化)它通过避免在压缩期间进行任何实际数据复制来减轻写入放大。
假设 H-LSM 树级别 Lk 的大小超过了预定义的阈值。列表压缩以循环方式选择牺牲 NVTable,并在 Lk+1 级别中找到键范围与牺牲 NVTable 的键范围重叠的 NVTable(压缩到 L k + 1 L_{k+1} Lk+1层)。这些 NVTable 一起作为输入文件。
如图 7 所示,在第一步中,多个工作线程对所有键值记录执行并行就地样本排序。对于每个 NVTable,工作线程使用偏移量数组来收集一定数量的等距样本。然后,主线程对所有采样的 KV 对进行排序,并在排序数据上收集 k − 1(k 是工作线程编号)个等距样本(论文最开始好像是根据基数排序的方式)。
两个相邻样本的键的间隔表示工作线程的键范围。所有工作线程的键范围均不重叠。每个工作线程从每个 NVTable 中获取键范围内的关联数据,独立执行本地合并排序,并生成新的偏移数组。使用偏移数组元数据,每个工作线程执行就地排序而不会干扰 KV 数据位置。
在第二步中,每个工作线程使用偏移数组扫描键值对并执行数据重复数据删除。每个键值对都有一个序列号,该序列号是在 put/del 中创建的单调递增时间戳。如果它检测到具有相同键的多个键值记录,则使用序列号保留具有最新值的记录(哪个时间戳新保留哪一个,也是类似于增量查询的方式)。对于过时的 KV 对,在关联的偏移数组条目中设置删除标志。
所有 worker 完成排序后,主节点将 worker 的结果连接起来以生成全局偏移数组。
在第三步中,它将全局偏移数组划分为 m 个部分,其中 m 是输出 NVTable 的数量(此时实现了新低层级的NVTable生成)。它将分区数组分配给 worker。对于输出 NVTable,worker 将偏移数组复制到 NVTable 中,找到关联的 KV 列表节点并重新链接这些节点。
列表压缩会调整前后列表节点指针以移动键值对,而不是像传统 LSM 树压缩那样复制实际数据。由于写入放大主要是由键值数据写入引起的,因此列表压缩利用基于列表的数据组织来避免大量键值数据复制,并只需修改四个八字节指针即可进行列表节点调整。
键值数据排序不会影响就地排序的系统状态一致性。在第二步中,我们使用全局偏移数组进行数据重复数据删除(这里实现了对NVM最高层L0数据的排序)。旧版本的键值对在偏移数组条目中被标记为已删除。与第一步类似,此步骤对输入数据也没有副作用。在第三步中,我们使用原子 cmpxchg 和 clwb 指令来修改和持久化 KV 列表指针。它保证了 KV 列表节点链接期间的崩溃一致性。如果发生崩溃,恢复例程将使用偏移量数组重建不完整的 KV 列表。
NVM 的访问速度比 SSD/HDD 更快。因此,在 NVM 中保留热数据对于分割 LSM 树的性能尤为重要。为了实现这一点,我们提出了数据升级和降级机制,旨在将热键值记录从慢速 SSD 迁移到快速 NVM,并从 NVM 中逐出冷键值记录。
Data promotion
为了设计一个高效的数据提升机制,我们需要回答两个问题:
(1)如何精准识别热门键值对?
(2)何时将热门键值对提升到 H-LSM 树上?
为了回答第一个问题,我们使用 count-min sketch 方法 [29] 来检测 SSTable 中的热门键值数据。具体来说,我们为每个 SSTable 创建一个具有 m 行和 n 列的二维 sketch 数组。m 是哈希函数的数量。当线程读取一个键值对时,它会增加关联的 m 个 sketch 数组单元中的计数器。如果这 m 个单元中的最小计数器大于预定义的阈值,则表示该键值对是热门的,我们将其添加到易失性缓冲区中以供将来提升。
由于 SSTable 中的键值对是经过排序的,因此插入新的 KV 对会导致昂贵的数据移动。为了避免这种开销,我们将数据提升集成到 L-LSM 树压缩中。具体来说,假设压缩是将键值对从 Lk 迁移到 Lk+1。我们选择那些键位于输入 SSTables 的键范围内的热门 KV 对,并将它们添加到压缩输入中。此压缩将输出新的 SSTables 到 Lk+1(这里不应该是将热门KV对输出到Lk-1层吗)。因此,热门键值对从 Lk+2 提升到 Lk+1。在热门数据被提升到 SSD 中最高的 L-LSM 树级别后,我们需要将它们迁移到 NVM 中的 H-LSMtree。类似地,当压缩发生在最后两个 H-LSM 级别之间时,我们选择最高 L-LSM 树级别中的那些热门 KV 对并将它们添加到此压缩输入中。
此外,当前的 LSM 树压缩是热点无意识的。压缩可能会将热数据移至低级别。为了避免意外的数据沉降,我们设计了一个热点意识的 L-LSM 树压缩。对于 Lk-Lk+1 压缩,我们将输出键值对分为热集和冷集。在压缩输出期间,热的键值对保留在Lk,而冷的键值对移动到Lk+1。
Data Demotion
除了热数据提升之外,我们还将冷键值数据从 NVM 降级到 SSD。**我们采用基于 LRU 的粗粒度数据驱逐策略来降级 H-LSM 树数据,**而不是为 L-LSM 树使用细粒度的热数据草图。我们的动机是 LSM 树工作负载通常是写入密集型的。因此,更新频率最低的 NVTable 包含最冷的数据,应该被驱逐到 SSD。具体来说,我们为存储在 H-LSM 树级别的 NVTable 构建一个全局 LRU 列表。新创建的 NVTable 被插入到 LRU 列表头中,因为它是最热的。当压缩发生在 H-LSM 树的最低级别时,它会选择位于 LRU 列表尾部的 NVTable,并将 NVTable 数据压缩到底层 L-LSM 树。
我们利用建议的分割 LSM 树构建 SplitDB。SplitDB 将新兴的 NVM 技术集成到现代键值数据库中,以实现热点优化、写入效率和崩溃一致性。
SplitDB 并行化键值数据插入以提高用户写入性能。LevelDB 序列化 MemTable 插入。当多个并行线程执行 MemTable 插入时,委托线程会收集所有线程的请求。它会逐个将键值对记录到 WAL 文件中。在 WAL 文件持久化后,委托线程将键值对插入 MemTable。WAL 文件写入和 MemTable 插入都是序列化的。RocksDB 通过并发 MeTable 插入缓解了这个可伸缩性问题。但是,其 WAL 文件写入仍然是序列化的。
与 RocksDB 类似,SplitDB 也允许并行 MemTable 插入。此外,SplitDB 中没有 WAL。**它利用崩溃一致性 KV 列表来确保数据放入/删除的原子持久性。KV 列表插入是无锁的。**多个线程使用 cmpxchg 指令以原子方式将数据附加到 KV 列表。之后,成功的 KV 列表附加使用 clwb 来保证数据的持久性。
键值数据库应提供多版本数据管理,以实现正确的范围查询。我们使用与 LevelDB 相同的版本定义,即版本**是两个相邻压缩之间的 SSTables/NVTables 集**。压缩将生成许多新的 SSTables,但它们对于在压缩之前启动的读取器是不可见的。每次压缩完成时,LevelDB 都会创建一个具有 L 行和 k 列的二维版本数组。L 和 k 分别是 LSM 树的高度和所有级别的最大 SSTable 数量(并不完全是是一个矩形)。版本数组跟踪 SSTables,数组条目是 SSTable 文件名。在 SplitDB 中,其 L-LSM 树采用与 LevelDB 相同的管理方案。对于其 H-LSM,我们提出了一种基于时间戳的轻量级多版本数据管理策略。
具体来说,图 8 显示 SplitDB 维护一个全局时间戳 T,它是一个单调递增的整数。
每次 SplitDB 完成压缩或 ImmuTable 刷新时,它都会将 T 增加一。每个全局时间戳都有一个关联的引用计数器(图中的Ref)。读取器(例如,读取或扫描 NVTable)在开始/完成 NVTable 读取时会增加/减少计数器。每个 NVTable 都有两个本地时间戳:创建时间戳 T c T_c Tc和逻辑删除时间戳 T l T_l Tl(NVTable 自身的记录)。
压缩会删除旧的 NVTable 并生成新的 NVTable。假设当前全局时间戳为 T。已删除 NVTable 的逻辑删除时间戳设置为 T,而新创建的 NVTable 的创建时间戳设置为 T + 1。
在 SplitDB 中,NVTable 使用元组 [max_key, Tc] (key的最大值,创建时的时间戳)进行跳跃列表索引。max_key 是主键。对于具有相同 max_key 的 NVTable,它们按 Tc 降序排序。
如图 8 所示,假设读取器从 T8 开始。只有在 T8 之前创建的 NVTable 集对读取器可见(因为之前的NVTable失效了)。读取器在 ① 中增加相应的计数器。然后,它使用请求的键 10 和时间戳 T8 遍历跳跃列表。它找到第一个元组大于或等于 [10, T8] 的节点 (②)。
元组比较规则定义如下。对于两个元组 t1:[key1,T1] 和 t2:[key2,T2],(1) 如果 key1 < key2,则 t1 < t2。(2) 如果 key1 = key2 且 T1 > T2,则 t1 > t2。对于 (1),我们通过将 T8 与 NVTable 的创建和逻辑删除时间戳进行比较来进一步检查 NVTable 的有效性 (③)。
如果 Tc ≤ T8 ≤ Tl,则此 NVTable 可见。否则,如果 Tl 对于 (2),我们还验证了关联的 NVTable 的逻辑删除时间戳。 无论哪种情况,如果 NVTable 验证失败,读取器都会继续遍历底层列表 (④),访问 NVTable 的最大键大于或等于情况 (1) 或 (2) 中的键的后续节点,并验证时间戳 (⑤)。 Get[key]:Get[key] 可能导致涉及 DRAM MemTable、ImmuTable、NVM H-LSM 树甚至 SSD L-LSM 树的多层查找。假设 get 请求从 T 开始。 它首先探测 MemTable 和 ImmuTable。如果未命中,它将搜索 NVM H-LSM 树。对于每个 H-LSM 树级别,它会在级联跳跃列表中搜索第一个键大于 key 的节点。接下来,它通过验证创建和逻辑删除时间戳来找到有效的 NVTable。**如果验证成功,它会在有效的 NVTable 中搜索请求的键。**如果在 H-LSM 树中查找未命中,它将继续在 SSD 中搜索 L-LSM 树。查找过程与 LevelDB 相同。唯一的区别是它首先检查提升缓冲区,然后再搜索 SSTable。 Put[key, val]/Del[key]:Put 或 Del 会创建新的键值记录。 Del 为值创建墓碑标志。如第 III-D 节所述,此键值记录持久保存在持久性 NVTable 中,然后以严格的顺序插入易失性 MemTable 中。 Scan[min, max]:扫描操作创建一个迭代器。此迭代器遍历所有拆分 LSM 树级别以收集键值对。假设 Scan[min, max] 从 T 开始。首先,它在每个拆分 LSM 树级别定位起始遍历位置。在 H-LSM 树中,它使用 min 搜索跳跃列表以找到当前级别上第一个最大键大于或等于 min 的 NVTable。如果有多个匹配的 NVTable,类似于 get,它会检查它们的有效性。按照与 LevelDB 相同的方法,它在每个 L-LSM 树级别定位起始 SSTable。然后,迭代器开始在 NVTable/SSTable 中读取给定范围 [min, max] 内的键值对。如果读取到多个不同树层级的相同key的KV对,则返回序号最大的对,且序号应该小于T,该对包含最新的数据版本,迭代器持续扫描,直到当前读取的key超过[min,max]。 系统突然崩溃后,SplitDB 恢复例程开始将数据库恢复到一致状态。首先,它在 NVM 中恢复 H-LSM 树。恢复例程扫描每个 H-LSM (高层级树)树级别中的每个跳跃列表底层,以在 DRAM 中重建上层。接下来,它恢复在 ImmuTable 刷新之前创建的这些不完整的 NVTable。它添加 KV 列表节点的缺失后向指针并生成偏移数组。之后,它将恢复的 NVTable 插入到 H-LSM 树 L0 级。对于压缩期间生成的不完整 KV 列表,恢复例程使用全局偏移数组重新链接剩余的 KV 列表节点。接下来,LevelDB 恢复例程恢复 L-LSM 树。最后,SplitDB 还运行 PMDK 恢复例程来恢复 NVM 内存池状态。 SplitDB 是基于 LevelDB [2] 开发的。与 RocksDB 类似,我们为 SplitDB 添加了并发压缩支持。我们添加了大约 5700 行 C++ 代码来实现 NVM 中的 H-LSM 树。此外,我们还修改了 1200 行 LevelDB 代码,以实现数据升级和降级机制以及热点感知并发压缩。我们使用 Intel PMDK 库 [30] 来管理 NVM 空间。4.3 SplitDB KV operation
4.4 SplitDB Recovery and Implementation
footnote