文章目录
摘要
本文研究了B+ -tree如何充分利用内置透明压缩的现代存储硬件。近年来,人们对使用日志结构的合并树(LSM-tree)作为B+树的替代品产生了浓厚的兴趣,因为人们普遍认为LSM-tree在存储成本和写扩增方面具有明显的优势。本文的目的是在内置透明压缩的存储硬件到来时重新审视这一信念。先进的存储设备和新兴的计算存储驱动器执行基于硬件的无损数据压缩,对操作系统和用户应用程序透明。除了直接减少B+树和lsm树之间的存储成本差距之外,这种存储硬件为重新思考B+树的实现创造了新的机会。本文提出了三种简单的设计技术,可以利用这种现代存储硬件来显著减少B+树的写放大。在一个内置透明压缩的商用存储驱动器上的实验表明,提出的设计技术可以将B+树的写放大降低10以上。与RocksDB(建立在LSM-tree上的键值存储)相比,增强的B+树实现可以实现类似甚至更小的写放大。
一、Introduction
本文研究了B+ -树在一个不断增长的数据存储硬件家族上的实现,这些硬件内部执行基于硬件的无损数据压缩,对主机操作系统和用户应用程序透明。现代全闪存阵列产品(如Dell EMC PowerMAX[9]、HPE Nimble Storage[14]、Pure Storage FlashBlade[28])总是带有内置的基于硬件的透明压缩能力。内置透明压缩的商用固态存储驱动器正在出现(如,ScaleFlux[31]的计算存储驱动器和希捷[13]的Nytro SSD)。此外,云供应商已经开始将基于硬件的压缩能力集成到他们的存储基础设施中,例如,Microsoft Corsia[7]和新兴的DPU(数据处理单元)[5],导致内置透明压缩的基于云的存储硬件即将到来。有了专用的硬件压缩引擎,这种存储硬件可以在极低的延迟和零主机CPU开销的情况下支持高吞吐量的数据(解)压缩。
作为目前应用最广泛的索引数据结构,B+树[12]支持几乎所有的关系数据库管理系统。最近,日志结构的合并树(LSM-tree)[25]作为B+树的竞争者引起了人们的极大兴趣,主要是因为它的数据结构可以提高存储空间的使用效率,降低写放大。内置透明压缩的存储硬件的出现可以直接减少甚至消除B+ 树和LSM树之间的存储成本差距。本文表明,这种存储硬件还可以用于显著降低B+树写放大。关键是利用存储内透明压缩允许数据管理软件在不牺牲实际物理存储成本的情况下使用稀疏数据结构这一事实。在这种存储硬件上运行时,数据管理软件可以让4KB LBA(逻辑块地址)块部分填充甚至完全空,而不会浪费物理存储空间的使用。直观地说,使用稀疏数据结构的可行性为创新数据管理系统[36]创造了一个新的设计空间谱。
本文表明,B+ 树可以采用存储透明压缩的稀疏数据结构,极大地降低了其写放大。我们注意到,写放大是根据写入物理存储介质的数据量(即存储内压缩之后)来衡量的,而不是主机写入的数据量(即存储内压缩之前)。特别地,本文介绍了三种简单而有效的设计技术:
(1)确定性的页面遮蔽,可以确保B+ -tree页面更新原子性,而不会引起额外的写开销
(2)本地化的页面修改日志,可以减少由于B+树页面大小与数据修改大小不匹配而导致的写放大
(3)稀疏重做日志,可以减少由B+树重做日志(或预写日志)引起的写放大。随着写扩增的显著减少,B+树可以支持更高的插入/更新吞吐量,并更容易适应低成本、低持久性的NAND闪存(例如,QLC NAND闪存)。
因此,我们实现了一个包含了三种设计技术的B+树(称为B!-树)。我们进一步将其与LSM-tree (RocksDB[30])和normal B+ tree (WiredTiger[33])进行了比较。我们在带有内置透明压缩[31]的商业计算存储驱动器上进行了实验。结果很好地证明了所提出的设计技术在减少B+树写放大方面的有效性。例如,在每条记录128B的随机写工作负载下,RocksDB和WiredTiger(页面大小为8KB)的写扩增分别为14和64,而我们的B!-tree (8KB页面大小)的写倍数仅为8,分别比RocksDB和WiredTiger减少43%和88%。较小的写放大可以直接转换为更高的写吞吐量。例如,我们的结果显示,在随机写工作负载下,B!-tree可以实现约85K TPS(事务每秒),而RocksDB和WiredTiger的TPS分别为71K和28K。此外,我们注意到所提出的设计技术主要局限于B+树的I/O模块,并且与其他模块在很大程度上是正交的。因此,将这些技术合并到现有的B+树实现中相对容易。例如,在基线B+树实现上,我们只修改/添加了大约1200个LoC来实现B+树。
二、背景
2.1 B+树数据压缩
B+ 树以页面为单位管理其数据存储。为了降低数据存储成本,B+ 树可以应用块压缩算法(如lz4 [23], zlib [37], ZSTD[38])来压缩每个存储上的页面(如MySQL和MongoDB/WiredTiger中的页面压缩特性)。除了明显的CPU开销之外,B+树页面压缩还会由于4KB对齐约束而遭受压缩比损失,这可以解释为:现代存储设备以4KB LBA块为单位服务I/O请求。因此,每个B+ 树页面(无论压缩还是未压缩)都必须完全占用存储设备上的一个或多个4kB LBA块(即,没有两个页面可以共享一个LBA块)。当B+树应用页面压缩时,4kb对齐约束可能导致明显的存储空间浪费。如图1所示:假设一个16KB的B+树页面被压缩到5KB;压缩页必须占用存储设备上的两个LBA块(即8KB),浪费3KB存储空间。因此,由于4KB对齐约束导致的CPU开销和存储空间浪费,B+树页面压缩在生产环境中没有广泛使用。此外,众所周知,在随机写操作的工作负载下,B+树页面往往只有50%-80%的填充率[12]。因此,B+树通常具有较低的存储空间使用效率。相比之下,LSM-tree的数据结构更紧凑,在压缩时不受4kb对齐约束,因此比B+树具有更高的存储空间利用率。
2.2 存储内透明的压缩
图2为内置透明压缩的计算存储驱动器(CSD):在CSD控制器芯片内部,由硬件引擎直接在I/O路径上进行压缩解压缩,由FTL (flash translation layer)管理所有可变长度压缩数据块的映射。由于压缩是在存储驱动器内进行的,因此不受4kb对齐约束(即所有压缩块都紧密地封装在闪存中,没有任何空间浪费)。
如图3所示,内置透明压缩的存储硬件具有以下两个特性:
(a)存储硬件可以公开一个比其内部物理存储容量大得多的LBA空间。这在概念上类似于精简配置。
(b)由于某些数据模式(例如全0或全1)可以被高度压缩,我们可以在不浪费物理存储空间的情况下,让一个4KB LBA被有效数据部分填充。
这两个属性将逻辑存储空间利用率与物理存储空间利用率解耦。这允许数据管理软件在逻辑存储空间中使用稀疏的数据结构,而不牺牲真正的物理存储成本,这为数据管理系统[36]创建了一个新的设计空间范围。
2.3 B+树vsLSM tree
由于LSM-tree在存储空间使用和写放大方面的优势,它最近受到了极大的关注(例如,见[3,15,21,22,29,35])。如果B+树有一个非常大的缓存内存(例如,足够容纳整个数据集),并且使用非常大的重做日志文件,它的写放大可能比LSM-tree要小得多。此外,在较大的记录大小(例如1KB以上)下,B+树往往比lsm树有更小的写放大。因此,这项工作的重点是数据集远远大于缓存容量,同时记录大小倾向于小(例如,几百字节或更少),在这种情况下,B+树倾向于承受比lsm树更高的写放大。
为了演示,我们使用RocksDB和WiredTiger作为LSM-tree和B+ tree的代表,并在一个3.2TB的存储驱动器上进行实验,该驱动器内置ScaleFlux[31]的透明压缩。我们在150GB数据集上运行记录大小为128字节的随机纯写工作负载。对于WiredTiger,我们将其B+ 树叶页面大小设置为8KB。表1列出了LBA空间上的逻辑存储使用情况(即在存储内压缩之前)和物理存储使用情况(即在存储内压缩之后)。由于LSM-tree具有更紧凑的数据结构,RocksDB的逻辑存储空间使用量比WiredTiger更小(即218GB vs. 280GB)。然而,在存储透明压缩后,WiredTiger比RocksDB消耗更少的物理存储空间,这可能是由于LSM-tree的空间放大。图4显示了不同客户端线程数下的写扩增。我们用物理写入存储驱动器内NAND闪存的压缩后数据量与写入数据库的总数据量之间的比率来衡量写入放大。结果表明,RocksDB始终比WiredTiger少4倍数量的写放大。
以上结果表明,通过存储内透明压缩,我们可以缩小B+ tree和LSM-tree之间的物理存储成本差距,而LSM-tree在写放大方面仍然保持着显著的优势。这项工作的目标是通过适当修改B+ tree实现来进一步缩小写放大的差距。
2.4 B+树写放大
在当前的I/O接口协议下,存储设备只保证每个4KB LBA块的写原子性。因此,当页面大小大于4KB时,B+ tree必须自己确保页面写原子性,这可以通过两种不同的策略来实现:
(i)就地页面更新:尽管方便的就地更新策略简化了页面存储管理,但B+ tree必须相应地使用页面日志记录(例如,MySQL中的双写缓冲区)来避免页面写失败,从而导致大约2个更高的写量。(ii)写时复制(或遮蔽)页面更新:尽管写时复制避免了页面日志记录的使用,并且很容易支持快照,但它使页面存储管理复杂化。同时,B+ tree必须使用某些机制(例如页映射表)来跟踪页的位置,这仍然会引起额外的存储写流量。
据此,我们可以将B+ -树存储写流量分为三类:(1)日志记录写到,确保事务原子性和隔离,(2)页写道,存在内存中的肮脏的B +树页存储设备,和(3)引起的额外写道,确保页面写原子性(例如,页面日志的就地更新,或页面映射表持续的跟踪页)。让Wlog、Wpg、We表示这三种类型的总数据写入量,Wusr表示写入B+ -树的用户数据总量。我们可以把B+ -树的表达式写放大为据此,我们可以将B+ -树存储写流量分为三类:(1)日志记录写到,确保事务原子性和隔离,(2)页写道,存在内存中的肮脏的B +树页存储设备,和(3)引起的额外写道,确保页面写原子性(例如,页面日志的就地更新,或页面映射表持续的跟踪页)。让Wlog、Wpg、We表示这三种类型的总数据写入量,Wusr表示写入B+ -树的用户数据总量。我们可以把B+ -树的表达式写放大为
当B+ -tree运行在内置透明压缩的存储硬件上时,让αlog、αpg和αe表示这三类写操作的平均压缩比。这里我们通过将压缩后的数据量除以压缩前的数据量来计算压缩比。因此压缩比总是在(0,1)之间,数据可压缩性越高,压缩比就越小。因此,整体的B+ 树写放大成了
3 已提出的设计技术
根据公式(2),我们可以通过减少WAlog、WApg和/或WAe(即减少B+ -树写数据量)或减少αlog、αpg和/或αe(即提高写数据的可压缩性)来减少B+树写放大。通过应用存储透明压缩支持的稀疏数据结构,本节介绍了三种减少B+树写放大的设计技术:(1)确定性页面遮蔽,消除WAe,(2)本地化页面修改日志,减少WApg和αpg,(3)稀疏重做日志,减少αlog。
3.1 确定的页面跟踪
为了消除WAe, B+ tree应该采用页面遮蔽的原则。然而,在页面阴影的传统实现中,每个更新的B+ tree页面的新的存储位置是在运行时动态确定的,必须记录/持久化,这导致了额外的写开销和管理复杂性。为了消除额外的写开销,同时简化存储管理,我们提出了一种称为确定性页面遮蔽的技术,如图5所示:让lpg表示B+ -树页面大小(例如,8KB或16KB)。对于每个页面,B + -树分配2lpg的逻辑存储区域在LBA空间划分成两个size-lpg插槽(位置0和槽- 1)。对于每个B+ -tree页面,逻辑存储空间上固定位置的两个槽位以乒乓的方式交替刷新内存到存储的页面。一旦页面从内存中刷新到一个槽位,B+ -tree将对另一个槽位发出一个TRIM命令。这在概念上与传统的页面阴影相同,只是阴影页面的位置现在是固定的。尽管B+ -tree占用了2个更大的逻辑存储空间,但只有一半的存储空间用于存储有效数据,另一半被修整(因此不消耗物理闪存存储空间)。正如上面2.2节所指出的,内置透明压缩的存储硬件可以公开一个比其内部物理存储容量大得多的逻辑LBA存储空间。因此,这种存储硬件可以很容易地支持确定性页面遮蔽。我们注意到确定性页面遮蔽仅仅是为了确保页面写原子性而没有额外的写开销。为了支持多版本并发控制(MVCC), B+ -tree可以使用常规方法,如undo logging。
在提出的确定性页面遮蔽中,B+ -tree使用内存中的位图来跟踪每个页面的有效槽位。与传统的页隐藏使用的页表相比,位图占用的内存资源要少得多。此外,B+ -tree不需要持久化位图。当系统重新启动时,B+ -tree可以逐步重建内存中的位图:当B+ -tree第一次加载一个页面时,它从存储设备读取两个插槽。对于修剪槽位,存储设备只返回一个全零块,B+ -tree可以根据该块轻松识别有效的槽位。当B+ -tree读取页面的两个插槽时,存储设备内部只从物理存储媒体获取有效(即未修剪)插槽。因此,与读取一个插槽相比,读取两个插槽只会通过PCIe接口招致更多的数据传输,而不会在存储设备内部产生任何额外的读取延迟。这应该不是问题,因为即将推出的PCIe Gen5将支持16GB/s 32GB/s,这远远大于存储设备内的后端闪存访问带宽,因此可以很容易地容纳额外的数据传输。当系统崩溃时,B+ -tree需要处理以下两种可能的情况:(i)某个槽位在系统崩溃前被部分写入:B+ -tree可以通过验证页面校验和很容易地识别出被部分写入的槽位。(ii)一个槽位已经写入成功,但另一个槽位在系统崩溃前还没有被修剪:B+ -tree可以通过比较两个槽位上的页面LSN(逻辑序列号)来识别有效的槽位。因为没有必要持久化内存中的位图,所以确定性的页面遮蔽从总的B+树写放大中消除了αe·WAe组件。
3.2 本地化页面修改日志
第二种技术旨在减少公式(2)中的αpg和WApg成分。它的动机是一个简单的观察:对于B+ -tree页面,让我们表示其在内存中的图像和在存储中的图像之间的差异。如果差异显著小于页面大小(即|?| <<Lpg),通过记录页面修改?,而不是将整个内存中的页面图像写入存储设备,我们可以在很大程度上减少写入放大。这在概念上与基于相似性的数据重复删除[2]和增量编码[24]相同。不幸的是,当B +树运行在正常存储设备没有内置透明压缩,这种方法是不实际的,因为重要的操作开销:对于4KB块IO接口,我们必须将来自不同页面的多个?合并成一个4KB LBA块,以实现写放大减少。为了提高效果,我们应该对每个页面应用多次页面修改日志记录,然后重新设置此过程以构建最新的存储上页面映像。因此,与同一页面关联的多个?将分布在存储设备上的多个4KB块上,但这会导致两个问题:
(1)对于每个页面,B+ -tree必须跟踪其所有相关的?,并定期进行垃圾收集,导致存储管理的复杂性很高。
(2)要从存储器加载一个页面,B+ -tree必须从多个不连续的4KB LBA块读取现有的存储上的页面图像和多个?,这导致了很长的页面加载延迟。
因此,据我们所知,这个简单的设计概念还没有被实际的B+ -树实现所使用,甚至在开放文献中也没有报道过。
内置透明压缩的存储硬件首次使上述简单的想法切实可行。通过应用这种存储硬件支持的稀疏数据结构,我们不再需要将来自不同页面的多个?合并到相同的4KB LBA块中。利用丰富的逻辑存储LBA空间,对于每个B+树页面,我们可以简单地指定一个4KB LBA块作为它的修改日志空间来存储,这称为本地化的页面修改日志。在4KB I/O接口下,为了实现每个页面的页面修改日志记录,B+ -tree将D = [?,O](其中O表示全零向量,|D|为4KB)写入与页面相关的4KB块。在存储设备内部,D中的所有0都将被压缩,只有压缩后的版本将被物理存储。因此,当使用页面修改日志记录服务于每个内存到存储的页面刷新时,我们通过向逻辑存储LBA空间写入4KB而不是lpg数量的数据来减少WApg,并降低压缩比αpg,因为写入的数据[?,O]可以被存储设备高度压缩。通过为每个B+ -tree页面提供一个4KB的修改日志空间,我们不会产生额外的B+ -tree存储管理复杂性。读放大较小主要有两个原因:(1)B+ -tree总是只读取一个额外的4KB LBA块。而且,每个页及其关联的4KB日志块连续地驻留在LBA空间上。因此,为了同时读取页面及其关联的4KB日志块,B+ -tree只向存储设备发出单个读请求。(2)存储设备内部从闪存中取出非常少量的数据,以重建4KB LBA块[?,O]。
为了实际实现这个简单的想法,B+ -tree必须执行两个额外的操作:(1)为了将页面从存储器加载到内存中,B+ -tree必须基于存储上的页面映像和构造最新的页面映像。(2)为了将一个页面从内存刷新到存储,B+ -tree必须获取并相应地决定是否应该调用页面修改日志。为了最小化B+ -树的操作开销,我们提出了以下实现策略:让Pm和Ps表示一个B+ -树页面的内存中和存储上的图像。我们逻辑地将Pm和Ps划分为k个段,即Pm = [Pm,1,···,Pm,k]和Ps = [Ps,1,···,Ps,k],以及|Pm,i| = |Ps,i| i(即Pm,i和Ps,i在同一位置大小相同)。对于每一页,B+ -tree保持一个k位向量f = [f1,···,fk],当Pm,i = Ps,i时,fi设为1。相应地,我们通过连接内存中的所有段Pm,i和fi = 1来构造。在运行时,每当内存页中的第i段被修改时,B+ -tree将其对应的fi设置为1。当B+ -tree将页面从内存刷新到存储时,它首先计算?的大小
我们定义一个不大于4KB的固定阈值T。如果|?|<= T,那么B+ -tree将调用页面修改日志记录,其中可以通过简单的内存复制操作获得。我们注意到,k位向量f应该与专用的4KB页面修改日志块一起写入。当B+ -tree将页面从存储加载到内存时,它从存储设备获取lpg + 4KB的数据,其中size-lpg空间包含当前存储上的页面图像Ps,而额外的4KB块包含相关的f和?。因此,我们可以通过简单的内存复制操作轻松地构造最新的页面映像。对于每个B+ -tree页,它的大小将随着B+ -tree经历更多的写操作而单调地增加。一旦|?|大于阈值T,我们将通过使用?= /0和f为全零向量将整个最新页刷新到存储中来重置该进程。我们注意到,阈值T配置了写放大减少和存储空间放大之间的权衡:当我们增加T的值时,可以减少重置页面修改日志记录过程的频率,从而减小写放大。同时,在T值较大的情况下,日志空间中会累积更多的页面修改,从而导致更大的存储成本开销。
图6进一步说明了该实现策略。在所有的k段中,第一个段Pm,1是页眉,最后一个段Pm,k是页尾,两者都可以比其他段小得多。假设页面更新导致段Pm、3和页眉/页尾的修改。当B+ -tree从内存中清除该页时,它将?构造为[Pm,1,Pm,3,Pm,k],并写入?和k位向量f到专用4KB块日志块,在存储设备内进一步压缩。
我们注意到,如果B+ -tree将内存中的页面视为不可变的,并使用内存中的delta链来跟踪内存中的页面修改(这在Bw-tree[19,20]中用于实现无锁存操作),我们很有可能进一步减少|?|,从而提高本地化页面修改日志记录在减少写扩增方面的有效性。然而,这种增量链接方法会极大地使B+ -tree实现[32]复杂化,并引起明显的内存使用开销。因此,本工作在我们的实现和评估中选择了上述简单的基于页面内段的跟踪方法。
3.3稀疏重做日志
第三种设计技术旨在减少Eq.(2)中的分量αlog(即提高重做日志数据的压缩性)。为了最大限度地提高可靠性,B+ -tree在每次事务提交时都使用fsync或fdatasync刷新重做日志。为了减少日志引起的存储开销,传统做法总是将日志记录紧密地打包到重做日志中。因此,多次连续的重做日志刷新可能会写入存储设备上的同一个LBA块,特别是当事务记录显著小于4KB和/或工作负载并发性不是很高时。这可以在图7中说明:假设三个事务TRX-1、TRX-2和TRX-3(日志记录L1、L2和L3)分别在t1、t2和t3提交,其中t1< t2 < t3 。如图7所示,在t1时刻,4KB数据[L1,O]从内存中的重做日志缓冲区刷新到存储设备上的LBA 0x0001,进一步内部压缩数据。随后,日志记录L2被追加到重做日志缓冲区,在t2时,4KB数据[L1,L2,O]被刷新到存储设备上相同的LBA 0x0001。同样,在t3时刻,4KB数据[L1,L2,L3,O]在存储设备上被刷新到相同的LBA 0x0001。如图7所示,相同的日志记录(如L1和L2)被多次写入存储设备,从而导致更高的写入放大。同样的,当每个4KB的重做日志缓冲区块中积累了更多的日志记录时,重做日志数据压缩比αlog会随着多次连续的重做日志刷新而变得越来越差。
通过应用存储硬件内置透明压缩支持的稀疏数据结构,我们提出了一种称为稀疏重做日志的设计技术,它可以使存储硬件最有效地压缩重做日志,从而减少日志导致的写放大。它的基本思想非常简单:在每个事务提交和相应的重做日志内存到存储刷新时,我们总是在内存中的重做日志缓冲区中填充0,使其内容达到4kb对齐。因此,下一条日志记录将被写入重做日志缓冲区中的一个新的4KB空间。因此,每条日志记录只写到存储设备一次,与传统做法相比,写放大程度较低。这可以在图8中进一步说明:假设与图7中所示相同的场景,在事务TRX-1在t1提交后,我们将0填充到重做日志缓冲区中,并将4KB数据[L1,O]刷新到存储设备上的LBA 0x0001。随后,我们将下一个日志记录L2放在重做日志缓冲区的一个新的4KB空间中。在时刻t2, 4KB数据[L2,O]被刷新到存储设备上新的LBA 0x0002。同样,在t3时刻,4KB数据[L3,O]被刷新到存储设备上的另一个新的LBA 0x0003。显然,每个重做日志记录只写到存储设备一次,并且重做日志写可以被存储硬件更好地压缩,导致一个更小的α日志,因此更低的写放大。由于在常规日志记录和稀疏日志记录中,每个事务提交总是调用对存储设备的一次4KB写操作,因此Eq.(2)中的重做日志总写量Wlog将保持不变。因此,通过降低对数压缩比αlog,提出的稀疏测井减少了总B+ -树写放大中的αlog·Wlog分量。