BzTree: A High-Performance Latch-free Range Index for Non-Volatile Memory

(一)研究目的

为 NVM 设计无锁(latch-free / lock-free)索引结构,以充分利用 CPU 的并行性。

(二)研究背景

将数据库(行和索引)完全存储在非易失性内存(NVM)中,有可能实现高性能和快速恢复。为了充分利用现代cpu上的并行性,现代主存数据库使用无锁(lock-free)索引结构,例如 Bw-treeskip lists。为了实现高性能,NVM-resident index 也需要是无锁的。
拓展:
Bw-Tree技术解读
讲讲跳跃表(Skip Lists)
跳表(SkipList)数据结构介绍

(三)研究概述

本文介绍了一种为 NVM 设计的无锁b树索引 BzTree 的设计。BzTree 使用一个持久的多字比较和交换操作(persistent multi-word compare-and-swap:PMwCAS)作为核心构建块,使得无锁索引设计与竞争索引结构 (如Bw-tree) 相比有以下几个优势:
(1)BzTree 是无锁存的,但实现起来很简单。
(2)BzTree 速度很快,在本文实验中,它的吞吐量比 Bw-tree 高出两倍。
(3)BzTree不需要任何特殊目的的恢复代码。恢复几乎是瞬时的,只涉及回滚(或前滚)故障期间运行中的任何 PMwCAS 操作。在 BzTree 的端到端恢复实验中,平均恢复时间为145µs。
(4)相同的 BzTree 实现可以在 volatile RAM 和 NVM 上无缝运行,大大降低了代码维护的成本。

PMWCAS:persistent multi-word compare-and-swap

BzTree 依赖于一种名为 PMwCAS 的高效和持久的多字比较和交换操作,以一种无锁存和持久的方式更新状态。该方法使用一个描述符来跟踪操作的元数据,这些描述符被池化并最终被重用。PMwCAS 的 API 如下:
PMwCAS API
使用PMwCAS,应用程序首先分配一个描述符,并为每个要修改的单词调用 AddWord 或 ReserveEntry 方法一次。如果需要,它可以使用 RemoveWord 删除以前指定的单词。AddWord 和 ReserveEntry 确保目标地址是唯一的,如果不是,则返回一个错误。调用 PMwCAS 将执行该操作,而 Discard 将终止该操作。当在 NVM 上运行,在电源故障时,一个失败的 PMwCAS 将保持所有目标单词不变。

(四)关键技术

4.1 Node Layout

BzTree节点遵循典型的slotted-page layout,其中固定大小的元数据(metadata)“向下”增长到节点中,而可变长度的存储(键和数据)“向上”增长。一个结点包括:

  • (1)A fixed-size header
  • (2)an array of fixed-size record metadata entries
  • (3)free space that buffers updates to the node
  • (4)a record storage block that stores variable-length keys and payloads

所有固定大小的元数据都被打包成64位对齐的字,因此可以使用 PMwCAS 以无锁存方式更新元数据。HeaderRecord metadata array 的详细信息如下图所示:
Node layout and details for the BzTree.
Free space:空闲空间用于吸收对节点的修改,比如插入记录。这个空闲空间位于固定大小的 variable-length payloads 和 record storage block 之间。record metadata array “向下”增长到这个空间,而 record metadata array “向上”增长。但是,内部索引结点不包含空闲空间,内部结点是搜索优化的,因此不会缓存更新,因为这样做会降低二进制搜索性能。

Record storage block:该块中的条目由连续的 key-payload 对组成。键是可变长度的字节字符串。内部 BzTree 结点中的有效负载是固定长度(8字节)的子结点指针。在本文中,假设存储在叶节点中的有效负载是8字节的记录指针(这在主存数据库中很常见)。但是,BzTree 还支持在叶结点中存储完整的 variable-length payloads。

Status Word,64位,用于存储在更新期间更改结点元数据。叶子结点和内部结点的 Status Word 的格式是不同的。
叶子结点:

(1)PMwCAS 控制位(3位)用于自动更新单词
(2)一个冻结的标志(1位),表明节点是不可变的
(3)记录计数字段(16位),存储记录元数据数组中的条目总数
(4)一个块大小字段(22位)存储占用的字节数的记录存储块的结束节点
(5)删除大小字段(22位)存储逻辑删除空间节点的数量,用于决定何时合并或重组节点

内部结点:

只包含(1)和(2)。因为在内部结点上不执行单例更新,因此不需要其他字段。出于搜索性能的考虑,选择批量替换内部结点(例如,添加或删除记录时)。

叶子结点和内部结点的不同点:

(1)Status Word 的格式不同
(2)内部节点一旦创建就不可变,而叶节点则不是
(3)内部节点只存储按键排序的记录(用于快速二分搜索),不包含空闲空间。而叶子节点包含用于缓冲插入的空闲空间(如果叶子节点存储完整的 record payloads,则更新),这意味着叶节点既包括已排序的记录(在结点创建期间出现的记录),也包括未排序的记录(增量地添加到页面的记录)。
B+树中绝大多数的更新发生在叶子级别,因此让叶子结点快速“适当”地吸收记录更新是很重要的。另一方面,内部索引结点的读取次数多,更改次数少,因此可以容允许批量替换,例如,当结点拆分后添加一个新密钥时。根据经验,保持内部索引节点搜索优化比使用已排序和未排序键空间组织内部结点的替代方法具有更好的性能。

4.2 Leaf Node Operations

叶子结点上的写操作,基本思想是使用 PMwCAS 以 latch-free 的方式操作页面并原子地记录元数据,既保留空间(在将可变长度数据复制到页面的情况下),又使更新对访问页面的并发线程“可见”。读操作(Readers)访问页面无竞争,也不会被写操作(writers)阻塞。表1 总结了与所有树操作相关的 PMwCAS 操作的大小。
Table 1: PMwCAS Summary Table

4.2.1 Inserts

New records 被添加到结点中可用的空闲空间中。要插入新记录 r,线程首先读取 the frozen bit。如果 frozen 设置了,这意味着页面是不可变的,可能不再是索引的一部分(例如,由于并发节点删除)。在这种情况下,线程必须重新遍历索引,以找到“活的”叶子结点。此外线程将在 record metadata array 和 record storage block 中为 r 预留空间。为 r 预留空间通过在以下字段执行一个 2-word PMwCAS 实现:

(1)在结点的 status word 上执行一个 2-word PMwCAS 来使 record count field 自动增加1,将 r 的大小添加到 block size;

(2)在 record metadata array entry 上执行一个 2-word PMwCAS 来翻转(flip) offset 的高阶位,设置其余位为全局索引 epoch。

如果这个 2-word PMwCAS 成功,则预留成功。offset 字段在这个阶段被重写以记住分配的索引 epoch。我们将这个值称为 allocation epoch,并将其用于恢复目的。高阶位(set/unset)用来表明这个值是一个 allocation epoch (set)还是一个实际的 record offset (unset)。

插入过程将 r 的内容复制到 storage block中 ,并更新相应 record metadata entry 中的字段,将 visible 标志初始化为 0 (不可见)。一旦复制完成,如果索引必须确保持久性,则线程刷新 r (使用CLWB或CLFLUSH)。然后读取 status word 的值 s 来再次检查 frozen bit,如果页面被冻结(例如,由于并发结构修改),就中止并重新尝试(?)。否则,通过以下两步来检测(谁?)与 试图设置 frozen bit 的并发线程的冲突:

(1)在 64-bit record metadata entry 上执行一个 2-word PMwCAS 来设置 visible bit,并将 offset 字段设置为实际的 record block offset (其高阶位未设置)
(2)设置 status word 为 s (与最初读取的值相同)

如果 2-word PMwCAS 成功,则插入成功。否则,线程将重新读取 status word (确保未设置 frozen bit ),并重试 PMwCAS。

Concurrency issues.
BzTree 必须能够检测到相同键的并发插入,例如:执行唯一键约束。本文实现了一个乐观协议来检测并发的键操作,如下:当一个 insert 操作首先访问一个结点时,它会在已排序的键空间中进行搜索,如果该键存在,则会中止插入。否则,它将通过扫描未排序的键空间继续搜索。如果搜索到任何未设置 visible flag 且 allocation epoch 的值等于当前全局索引 epoch 的记录,则可能是同一个键的正在进行的插入。一个带有 unset visible flag 且 allocation epoch 不等于 global index epoch 的条目,意味着在索引的前一个版本崩溃时,它要么被删除,要么正在进行分配,可以被忽略。线程设置一个内部的 recheck flag 来记住重新扫描未排序的键空间并继续进行插入,而不是等待正在进行的插入变得可见。如果线程丢失了一个 PMwCAS 来为其插入预留空间,那么 recheck flag 也会被设置,因为并发预留可能针对相同的键。在设置自己的可见位之前,如果设置了recheck flag,线程将重新扫描未排序的键空间,然后检查在它自己位置之前的所有条目。当遇到重复的键时,线程将其在 record storage block 中的条目置零,并将其 offset 的值设为零;这两个动作表示一个失败的操作,后续的搜索将忽略该操作。如果线程在它的扫描过程中遇到一个正在进行的操作,它必须等待记录变得可见,因为这个操作可能是在包含重复键的插入后面进行序列化。

4.2.2 Delete

删除一条记录:
(1)一个线程在 a record’s metadata entry 上执行 2-word PMwCAS ,取消其可见位(Visible),并将其偏移值(Offset)设为零,表示已删除的记录。
(2)一个线程在 the node status word 上执行 2-word PMwCAS ,增加 the delete size field 的大小为目标记录的大小
(increment the delete size field by the size of the target record)
如果 PMwCAS 由于并发删除或状态字(Status Word)上的冲突而失败,则线程将重试删除。如果失败是由于设置节点上冻结位(Frozen)的并发操作,则删除必须重新遍历索引,以便在可变叶节点上重试。增加删除大小(Delete Size)允许 BzTree 决定何时删除或合并节点。
在这里插入图片描述

4.2.3 Update

根据叶节点存储的是记录指针还是完整的有效负载,有两种方法可以更新现有的记录。
(1)Record pointers

如果叶节点包含记录指针,并且用户希望就地更新记录,则BzTree是被动的,更新线程可以简单地遍历指针,直接访问记录内存。如果更新需要交换一个新的记录指针,可以在记录存储块的适当位置进行。
为此,
(a)线程读取记录元数据条目m,以确保它没有被删除;
(b)线程读取状态字s,以确保节点没有被冻结。
然后
(1)在存储块中执行包含64位指针的 a 3-word PMwCAS 来安装新指针;
(2)执行包含记录的元数据条目的a 3-word PMwCAS,设置为m(the same
value it read) 来检测冲突与竞争删除试图修改这个词;
(3)执行包含状态字的a 3-word PMwCAS ,将它设置为 s (the same value it read),以检测与冻结位竞争翻转的冲突。(detect conflict with a > competing flip of the frozen bit)

(2)Inline payloads

如果叶节点存储了完整的有效负载,则更新遵循与插入相同的协议:
(1)在元数据数组和记录存储块中分配空间
(2)在描述更新的记录块中写入(key, update_payload)记录。
update_payload 可以是一个完整的负载替换,也可以是一个“字节差异”,仅描述负载中已经改变的部分。与insert不同,我们将对同一个键的并发更新视为一种自然竞争,支持“最后一个写入者获胜”协议。这意味着不需要检测对同一个键的并发更新。

4.2.4 Upsert

BzTree 支持大多数键值存储中常见的 upsert 操作。 如果记录存在于叶节点中,则线程对该记录执行更新。 如果该记录不存在,则线程执行插入操作。 在这种情况下,如果插入由于另一个并发插入而失败,则操作可以重试执行更新。

4.2.5 Reads

BzTree更新操作不会阻塞读取器。读取器将索引遍历到目标叶结点。如果叶结点存储记录指针,则线程优先对排序的键空间执行二进制搜索。如果它找不到它的搜索键(或者该键不存在,或者在已排序的空间中被删除),它将对未排序的键空间执行顺序扫描。如果找到键,它将把记录返回给用户。如果叶结点存储完整的记录有效载荷,则搜索首先从最近的条目开始扫描未排序的键空间,因为最近的更新记录将表示记录的最新有效载荷。如果没有找到键,则继续搜索已排序的键空间。

read只返回它在结点上找到的与搜索键匹配的最新记录。它忽略结点上的所有并发更新活动,忽略the frozen bit和任何正在进行的记录操作(unset visible bits)。这些并发操作被视为自然竞争,因为(a)任何记录级并发必须在 BzTree 之外处理,(b) the frozen bit对读取没有影响,因为the frozen bit被试图重组结点以序列化更新的操作使用。

4.2.6 Range Scans

BzTree支持如下的范围扫描。用户通过指定一个 begin_key 和一个可选的end_key(null if open-ended) 来打开一个扫描迭代器,定义他们希望扫描的范围。然后,一次扫描一个叶结点,直到终止。它首先进入一个 epoch 以确保内存稳定性,并使用 begin_key 查找初始叶节点。当进入页面时,迭代器构造一个响应数组,以排序的顺序列出结点上的有效记录(可见且未删除)。本质上,响应数组是节点的记录存储块中有效记录的快照副本。复制快照之后,迭代器退出它的 epoch,以便不保留内存垃圾回收。然后,它从快照中服务 record-at-a-time get_next请求。一旦耗尽响应数组,迭代器将继续到下一个叶结点,输入一个新的 epoch,并对响应数组中最大的键进行“大于”搜索遍历树;这个值表示前一个叶节点的高边界键,允许遍历查找扫描中的下一个叶节点位置。此过程重复执行,直到迭代器不能再满足用户提供的范围边界,或者用户终止迭代器。

4.3 Leaf Node Consolidation

由于插入或删除的副作用,叶子结点的搜索性能和有效空间利用率会下降。搜索降级是因为(a)需要顺序扫描未排序的键空间(在许多插入的情况下)和/或(b)大量删除添加到已排序的键空间中的“死空间”,从而增加了二分搜索的成本。BzTree偶尔会合并(重组)一个叶节点,以提高搜索性能和消除死区。当剩余空间达到最小阈值或节点上逻辑删除的空间量大于可配置阈值时,就会触发合并。

为了执行节点N的整合,线程首先对节点N的状态字执行 a single-word PMwCAS 来设置它的 frozen flag。这将防止完成任何正在进行的更新,并确保整合过程看到N记录的一致快照。然后,该进程扫描N以定位指向页面上所有活动记录的指针——忽略已删除和不可见的记录——并计算分配新节点所需的空间(所有有效记录的大小加上空闲空间)。如果该空间超出了可配置的最大页面大小,则进程调用节点分割(第5节将介绍)。否则,它将为新节点N’分配内存,并为新节点更新分配一些空闲空间缓冲。然后,它初始化报头,并按键顺序将所有活动记录从N复制到N’。现在,N’包含所有排序过的记录,可以替换N了。

要使N’在索引中可见,需要在父节点P上将指向N的指针替换成指向N’的指针。为此,线程使用它的路径堆栈(在遍历期间记录节点指针的堆栈)来找到指向P的指针。如果这个指针表示一个冻结的页面,则线程必须重新遍历索引以找到有效的父节点。然后,它在P中找到记录r,该记录r存储了指向N的孩子指针,并(1)在r中的64位孩子指针上使用2-word PMwCAS执行原地更新来安装指向N’的指针,(2)在P的状态词使用2-word PMwCAS执行原地更新来检测并发的页面冻结。如果这个PMwCAS成功,N’现在位于索引中,并且N可以被垃圾收集。但是,N不能立即被释放,因为这个进程是无锁存的,其他线程可能仍然有指向N的指针。正如在第2节中讨论的,BzTree通过使用epoch-based的垃圾收集方法来处理这种情况,以安全释放内存。

整合中的并发性。在整合之前冻结节点将导致该节点上的任何正在进行的更新失败,因为当尝试对状态字执行PMwCAS时,它们将检测到设置的冻结位。失败的操作将通过重新遍历树来重试,以找到新的“活动”叶节点。如果它们再次落在一个冻结的节点上,这是一个帮助完成整合的信号,而不是通过不断地重新遍历索引来“旋转”,希望得到一个活动的节点。在这种情况下,每个线程都将启动自己的整合过程,并试图将其安装在父进程中。这实际上使线程竞相安装合并的节点,尽管最终会有一个线程胜出。之后,每个线程恢复其原来的操作。

4.4 Internal Node Operations

对内部节点上现有记录的更新将按照前一节中讨论的协议执行,以安装新的孩子指针。为了保持内部节点的搜索最优性,可以记录插入和删除(例如,拆分或删除子节点的一部分),从而创建一个全新版本的内部节点。换句话说,内部节点中的插入或删除操作会立即触发合并。这个过程与刚才讨论的叶节点整合步骤相同:将创建一个新节点(添加或删除一条记录除外),它的指针将安装在父节点上。

4.5 Structure Modifications

BzTree中用于结构修改操作(SMOs)的锁存自由算法。与单节点更新一样,SMOs的基本思想是使用PMwCAS以原子方式和非锁存方式更新页面状态。这包括操作元数据(如冻结位),以及操作索引节点中的搜索指针以指向新页面版本(例如,分割页面)。

4.5.1 Prioritizing Structure Modifications

在BzTree中触发SMOs依赖于一个简单的确定性策略。一旦节点大小超过一个可配置的max_size阈值(例如,4KB),就会触发拆分。同样,一旦节点的大小低于可配置的min_size,就会触发节点的删除/合并。

如果一个更新线程遇到一个需要SMO的节点,它会在继续其操作之前暂停其操作以执行SMO(我们不强制读取器执行SMOs)。考虑到SMOs是相对重量级的,将它们优先于(轻量级的)单记录操作是很重要的。否则,在一场无闩锁的竞赛中,单记录操作将总是赢家,并有效地饿死SMOs。

4.5.2 Node Split

节点分裂分为两个阶段:
(1)准备阶段,该阶段使用SMO更改分配和初始化新节点
在这里插入图片描述
为了分割节点N,我们首先对其状态字执行PMwCAS来设置冻结位,如图3a所示。然后扫描N以找到所有有效的记录,并计算一个提供平衡分割的分隔键k。然后,分配和初始化三个新节点。
(1)一个新版的N(即N’)包含所有键值小于或等于k的有效记录
(2)一个新的同级节点O,它包含所有键值大于或等于k的有效记录
(3)N’的父节点P(即P’)用指向N’的指针取代N的孩子指针,并添加一个新的包含键k的搜索记录和一个指向新孩子O的指针。
所有节点都被整合(搜索优化)并存储有序记录。

(2)安装阶段,该阶段在索引中自动安装新节点
在这里插入图片描述

P’替换为P,从而使新的分裂节点N’和O在索引中可见。图3b描述了这个过程。安装是原子的,使用a 3-word PMwCAS修改下列单词:
(1)修改P的状态字设置其冻结位,失败则意味着它与P的另一个更新冲突,
(2) 修改在P的父节点G (N的祖父母)上指向P的64位孩子指针,替换为指向P’的新指针,
(3) 修改G的状态字来检测并发页面冻结。
如果PMwCAS成功,则分裂完成,旧节点P和N被发送到epoch-protected的垃圾收集器。如果失败,线程会重试分裂,节点N’、P’和O的内存会立即被释放,因为它们不会被其他线程看到。

4.5.3 Node Merge

BzTree 以类似于节点分裂的无锁存方式执行节点合并。在删除节点N之前,首先找到一个兄弟节点,它将吸收N的现有记录。选择N的左兄弟节点L,如果(1)它共享一个共同的父节点P,并且(2)小到足以吸收N的记录而不会触发分裂(违背合并的目的)。否则,我们查看N的右兄弟节点R,验证它有足够的空间来吸收N的记录而不进行分裂。如果R和L都不满足归并约束,允许N是欠满的,直到满足这些约束。

假设N与它的左兄弟节点L合并。
为了发起删除,我们首先对L和N的状态字执行 PMwCAS,设置它们的冻结位。然后分配和初始化两个新节点:(1)一个新的左兄弟节点L’包含自己的有效记录和所有N’的有效记录,(2)N和L的新父节点P’用指向L’的指针取代指向L的孩子指针,删除包换L和N之间分隔键的搜索记录,以及指向N的指针。

节点删除和合并的实现涉及到安装索引中新版本的 P’,使合并后的子节点L’可见并移除N和L。这个操作是与节点分裂相同,P’ 替换父节点 P,通过冻结 P 以及更新 P 的父节点 G 来安装指向 P’ 的新的孩子指针。

4.5.4 Interplay Between Algorithms

BzTree将ACID事务的处理卸给系统的更高层的软件层。例如,它可以是解耦数据库系统中的逻辑并发控制组件。索引本身负责正确地序列化冲突的数据和结构更改。

BzTree如何确保线程不会观察到正在进行的更改的影响?

Co-operative PMwCAS.
B+树的实现通常依赖于锁存来防止线程观察到并发线程执行的更改。BzTree使用PMwCAS来实现这一点。如2.3节所述,我们使用了一个无锁存的PMwCAS库。PMwCAS操作是协作的,因为任何遇到正在进行的PMwCAS的线程(读取器或写入器)都将首先帮助完成操作,然后再继续执行自己的操作。此策略有效地序列化了可能发生冲突的PMwCAS操作。它还确保了BzTree中操作的原子性。因为对索引的所有更新都是使用PMwCAS执行的,所以更新要么在无争用的情况下成功,要么PMwCAS help-along protocol(协议)将仲裁冲突并中止一些冲突操作。

Record operations and structure modifications.
BzTree使用状态词来正确地序列化相互冲突的数据和结构更改。例如,正在进行的合并或SMO将首先设置节点内的冻结位。这将导致所有飞行记录级操作由于状态字上的冲突而导致PMwCAS失败。然后,这些记录操作将重试,并看到(a)需要维护的节点的冻结版本,它将尝试完成该节点的冻结版本,或(b)节点的新(解冻)版本,准备进行记录更新。

Serializing structure modifications.
BzTree使用一种协作的方法来序列化冲突的SMOs。考虑节点删除操作。要删除节点N,BzTree首先检查它的左兄弟节点L是否存在。如果它观察到L被冻结,那么它就检测到另一个结构变化正在进行。在这种情况下,BzTree将N(如果仍然需要的话)的删除序列化到L之后。

4.6 BzTree Durability and Recovery

BzTree如何使用PMwCAS确保树跨系统故障的可恢复性?
当使用易失模式时,BzTree将树存储在DRAM上,当使用持久模式时,将树存储在NVM上。在易失模式下,BzTree不会将树的状态刷新到持久存储中。但是,当在持久模式下使用时,它将在NVM上持久化该树,以便在系统故障时保留它。BzTree不需要使用特定的恢复算法。相反,它依赖于持久内存分配器和PMwCAS库的恢复算法,以分别避免持久内存泄漏和确保可恢复性。现在我们详细描述这些算法。

4.6.1 Persistent Memory Allocation

当在NVM上使用时,带有分配和空闲接口的经典易失性内存分配器不能确保正确的恢复。如果分配程序将一个内存块标记为正在使用(由于分配),而应用程序(例如,BzTree)在崩溃之前未能将分配的内存块安装到NVM上,那么这将导致一个持久的内存泄漏。在这种状态下,内存块是“无家可归”的,因为在崩溃后,应用程序和内存分配器都不能看到它。

虽然创建一个安全而正确的持久化内存分配器超出了本文的范围,但是已经有很多建议了。我们假设有一个三阶段分配器可用,它提供以下状态:(1)分配,(2)激活,(3)空闲。应用程序首先请求分配一个内存块。分配器更新块的元数据,以表明它已被分配,并将其返回给应用程序。在系统故障后的恢复期间,分配器将回收所有已分配的内存块。为了在出现故障后仍然保留内存块的所有权,应用程序必须单独请求分配器激活内存块。此时,应用程序拥有内存块,并负责其生命周期,包括故障后的任何清理。

在激活过程中,应用程序必须通过一个类似于posix_memalign的接口(由分配器提供)小心地与分配器交互,该接口接受用于存储已分配内存地址的目标位置的引用。这种设计被许多现有的NVM系统所采用。只有当分配器成功地将新分配的内存地址持久化到所提供的引用中时,应用程序才拥有该内存。

4.6.2 Durability

BzTree在两种情况下处理索引数据的持久性。

Variable-length data.
新插入的记录以及新的节点内存(作为合并、分割或删除/合并的一部分分配)表示BzTree中的变长数据。为了确保持久性,bztree会在其他线程读取所有变长数据之前将其刷新。也就是说,在节点上新插入的记录内存在其可见位原子翻转之前被刷新。同样,在使用PMwCAS将新节点内存“链接到”索引之前,将刷新新节点内存。这个flush-before - visible协议确保了BzTree中的变长数据在并发线程可读时是持久的。

Word-size data.
字大小修改的持久性由PMwCAS操作处理。正如2.3节中提到的,PMwCAS确保了它在确认成功后修改的所有单词的持久性。因此,在使用PMwCAS进行修改时,可以保证诸如更改节点状态字以及保留和更新记录的元数据条目等修改是持久的。此外,PMwCAS执行的所有修改都保证对并发读取器是持久的。

BzTree避免了由读后写依赖引起的不一致性。也就是说,它保证一个线程不能读取另一个线程所做的易失性修改。否则,在读之后采取的任何操作(例如从属写)都可能在崩溃期间失效,并导致索引不一致。如上所述,在可见之前刷新协议确保对BzTree进行变长修改时使用此属性。类似地,PMwCAS确保对字大小的修改使用此属性。

4.6.3 Recovery

Memory lifetime.
PMwCAS库在NVM上定义良好的位置维护一个描述符池。每个单词描述符包含一个指定内存回收策略的字段。这个策略定义了当PMwCAS操作结束时,旧值和新值字段所指向的内存应该如何处理。PMwCAS库支持两种内存回收策略:NONE和FREE-ONE。使用前一种策略,不需要回收内存。BzTree使用此策略修改非指针值,例如节点中的状态字。使用后一种策略,PMwCAS库根据PMwCAS操作是否成功(或失败)释放旧(或新)值所指向的内存。BzTree在树中分配和安装新节点时使用此策略。为了激活节点内存,BzTree提供了对描述符单词的内存引用,描述符单词负责保存指向节点内存的指针。这确保了激活内存指针到描述符的原子传输。然后,内存生存期由PMwCAS库处理。如果出现故障,则由恢复算法回收节点的内存。这消除了BzTree实现自己的内存回收机制的需要。

Aborted space allocations.
虽然PMwCAS恢复可以处理64位字修改的恢复,包括指针交换和节点内存分配,但它不能处理节点内悬空记录空间分配的恢复。如4.2所述,一次插入被分解为两个原子部分:(1)记录空间分配和(2)记录初始化(复制键字节和填充元数据),并使记录可见。BzTree必须能够检测和恢复失败的插入,这些插入在(1)中分配了空间,但是在记录完全填充并显示之前,在(2)中崩溃了。BzTree使用分配epoch来实现这个目的(如4.2.1节所述,这个值临时存储在offset field中,直到(2)完成)。因为这个字段在(1)中是原子填充的,所以在恢复增加全局索引epoch之后,在(2)完成之前的任何后续失败都会被检测到。这样做将使任何遇到前一个epoch分配的搜索(例如插入检查重复键的搜索)无效。在合并或结构修改期间重建节点时,将回收此悬空节点空间。
Recovery steps.
在从系统故障中恢复期间,分配器首先运行它的恢复算法来回收已经保留但尚未激活的内存块。然后,PMwCAS 库执行其恢复算法,以确保所有成功完成的PMwCAS 操作的效果持续。如第2.3节所述,在崩溃后重新启动时,任何被标记为成功的运行中的 PMwCAS 操作将前滚,否则将回滚。对于涉及内存指针交换的操作,PMwCAS 将确保根据提供的内存回收策略正确处理由其描述符分配的和活动的内存解引用。

(五)实验环境

Environment

  • 大约 3000 行 c++ 代码实现了 BzTree,使用 PMwCAS 库来确保树更新的原子性和持久性。
  • PMwCAS 库使用 Win32 原生的 InterlockedCompareExchange64 来执行 CAS。
  • 基于新材料技术的 NVM 设备(如Intel 3D XPoint)还没有商业化。使用闪存支持的 NVDIMMs 来替代。NVDIMMs 是 DRAM,其数据内容在掉电时保存到闪存中。
  • 在一台运行 Windows Server 2012 操作系统的工作站上进行了实验,实验采用 Intel Xeon E7-8890 CPU(2.2GHz), 24 个物理核。

(六)总结

“传统的”无锁存设计依赖于一个 single-word CAS 指令,BzTree的设计,使用PMwCAS (多字比较和交换,保证了耐久性)有助于极大地降低索引设计的复杂性。尽管 PMwCAS 在计算上的开销比基于硬件的 single-word CAS 更昂贵,但使用 PMwCAS 获得的简单性不仅提高了可维护性,而且提高了 BzTree 的性能。

BzTree 设计的另一个好处是它的灵活性:同样的设计可以同时用于 volatile DRAM 和 NVM,大约8%的开销可以确保持久性。

在NVM上实现持久性的现有B+树实现通常使用复杂的算法来确保可恢复性。但BzTree不依赖于自定义恢复技术,而是依赖于通用的 PMwCAS 在联机和活动之前前滚(或后滚)它的 in-flight word modifications 。这允许几乎瞬时恢复BzTree 索引,这是其设计的一个定义特性。

本文文字描述过多,理解起来加大了难度,应多用一些操作的示意图和伪代码来加强可读性与可理解性。
PMwCAS 较难理解,但它又是核心操作,所以读懂了 PMwCAS 对读懂本文有很大帮助。PMwCAS 的设计基于 a volatile version by Harris et al [11],被作者增强以保证NVM上的持久性(details in [37])。所以对 PMwCAS 的理解还要看一下 [11] 和 [37] 两篇论文。

[11] T. L. Harris, K. Fraser, and I. A. Pratt. A practical multi-word
compare-and-swap operation. DISC, pages 265–279, 2002.
[37] T. Wang, J. Levandoski, and P . A. Larson. Easy lock-free indexing in
non-volatile memory. Technical report, Microsoft Research, 2017.

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值