The Bw-Tree: A B-tree for New Hardware Platforms 论文解读

The Bw-Tree: A B-tree for New Hardware Platforms 论文解读

Abstract

由于新硬件和平台的出现,驱使着人们重新考虑如何设计数据管理系统(DBMS)。文章设计了一种名为Bw-tree的新型B±tree,通过一种无锁的方法实现了非常高的性能,这种方法有效地利用了多核处理器缓存。同时使用一种独特的日志结构形式进行存储,模糊了页面存储和记录存储之间的区别。

Introduction

  • ARS(atomic record stores)通常用来描述一个 No-SQL 系统对 record 进行的原子性的增删改查操作,在一个实现了ARS 的 No-SQL 系统中,添加并发操作限制,可以实现出支持事务传统数据库,这也是目前许多数据库系统的构建思路。B-tree/B±tree 能够同时支持随机查找以及范围查找的 ARS,被认为是实现No-SQL 的高效数据结构。在数据库中,查找结构同时肩负着组织数据的任务,以B-tree和B±tree为例,这两种树形结构都是原地更新(In-place Update)的数据结构,即数据的更新发生在数据原先所在的位置上,需要以锁进行并发维护,在大规模的并发场景下,这种形式的存储结构必然带来瓶颈。文章提出的 Bw-tree 使用追加更新(Append Update )的形式,并且支持在无锁(latch-free)下的操作,文章主要的内容也是围绕此展开的,至于在更高层次上如何使用 Bw-tree 作为一个 LSM-tree,进一步支持各种事务操作,则并不是这篇文章的讨论内容。

  • 多核以及闪存,是Bw-tree能够有效工作的重要条件

BW-TREE ARCHITECTURE

这部分内容是对 Bw-tree 结构的粗略描述,里面的内容都会在后面章节详细介绍

在这里插入图片描述

Bw-tree 以如上的三层结构对数据进行组织,最上层的 Bw-tree Layer 对外层提供 API,负责对外交互。Cache Layer 保存一定数量的数据页在内存中,保存一个映射内存页和物理页之间的 Mapping Table,Storage Layer 以日志形式存储数据,可以存在闪存或者硬盘上,可以看作是持久化存储部分。

Bw-tree 在内存中读写时摒弃了对 latch 的使用,转而更多地使用 CAS。因为采用了 Append Update,也不需要担心 Cache layer中缓存失效率高的问题。其可能的性能瓶颈首先来自于 Flash Layer ,如果拥有足够大的内存,那么 Cache Layer 将足够大以至于不需要到 Flash Layer上去进行数据读取。I/O 访问速率决定了数据管理系统的性能,Flash Layer 采用闪存和一个较大的读写缓存区最大程度上提高性能。

Mapping Table 将 Bw-tree 中的页指针区分成两种类型,逻辑指针(logical pointer/inter pointer/PID)和物理指针(phycial pointer),并负责两者中的转换,核心在于,传统的 B-tree 中,树中的指针都是物理指针,当内存空间不足,发生内存管理切换时,树中的页结点可能被切换到 Flash Storage 中,这就需要树中的指针进行调整;在 Bw-tree 中,树中的指针值不会发生变化,变化只发生在 Mapping Table 中。Mapping table 有效地避免了对内存页的占用,使得内存管理更加灵活。

在 Mapping Table 的帮助下,Bw-tree 的并发地控制也转移到了 Mapping Table 上,Mapping Table 作为一个公共数据结构,采用 latch-free 的规则,都是通过 CAS 来控制并发操作。每一个线程在对物理页操作时,需要改变其在 Mapping Table 中的值,以阻止其他线程对之的同步操作。

Bw-tree 也不使用锁来保护 structure modifications (SMOs),即结构性的变化,比如叶节点的分裂或合并,Bw-tree 借用 B-link tree 的设计,在兄弟节点间添加指针,并将一个分裂的操作转变为几个 CAS 的组合,当一个分裂中的页被访问时,访问线程会检测到该页所处的状态,等待原子性操作的结束,进而保证安全性。

Bw-tree 的 LSS 通过只对新增的日志数据进行顺序刷盘,而不是将新增地日志添加到对应旧日志的后面,这样做尽可能地减少了 I/O 请求,但是问题也显而易见,对连续地读取会造成一定地影响。

Bw-tree 中的日志采用 LSN 进行标识,使用 WAL 缓存,对于并未提交的日志,并不会进行缓存,只会暂存在内存中,这降低对正常数据页缓存的影响。

IN-MEMORY LATCH FREE PAGES

这部分内容主要包括对 Bw-tree Layer 结构的讨论,包括弹性页面,更新与搜索操作,以及垃圾回收。

Elastic Virtual Pages

Bw-tree 的结构和 B±tree 的结构整体上是一致的,除过保存数据以外,叶节点上还保存节点上的最大 key 和最小 key,以及指向右侧兄弟节点的指针。Bw-tree 与传统的 B+tree的不同在于:(1)Bw-tree 中的页是虚拟页而不是物理地址上的的页;(2)Bw-tree 的页是弹性的,大小不断变化,没有限制。

不同于 B±tree 的原地修改,Bw-tree 的追加修改是在每个叶子节点上的 delta chain 上进行追加,具体的操作流程是对 Mapping Table 中的页指针进行获取,然后申请新的空间创建修改的 entry ,将 entry 连接到数据页的 delta chain,最后使用 entry 的地址作为数据页的最新地址,重新修改到 Mapping Table 上.

在这里插入图片描述

无论线程对指定 page 和 其delta chain 上进行那种操作,整个过程都采用 CAS 而不是利用锁,具体的比较值是页的物理地址是否是最新状态,这极大地提高了系统的并发效率。

链上的操作类型分为(1)插入(2)更改(3)删除。所有的操作 entry 都包含对应的 LSN,保证数据恢复。对键值的最新数据进行检索时,首先搜索 delta chain 上是否有关于这个 key 的最新数据,如果没有则在 Page 上进行类似于 B±tree 的二分查找。除此之外,delta chain 上还会添加一类跟页面整体操作相关的操作(delta)例如:(1)索引插入(2)页面删除.

Page Consolidation

弹性页面保持了在一定程度上的修改效率,但过长的 delta chain 会影响到整体的查询效率,因此页面合并压缩必不可少。当一个线程在搜索时检测到某个页面的 delta chain 的长度超多一定限度就会执行对页面的合并。

合并时,线程首先创建一个新的页面。然后用包含 delta chain 或旧的页面记录的最新版本的填充页面(删除记录将被丢弃)。然后,线程将更新 Mapping Table。如果成功,线程请求旧页面状态的垃圾收集(内存回收)。如果这个CAS失败,线程将通过释放新页来放弃该操作,该过程不会重试。

在这里插入图片描述

Range Scans

Bw-tree 支持范围搜索,对提供的 lowkey 与 high key 包含的范围进行搜索,其中任意一侧的边界可以省略。整个搜索过程由迭代器协助完成,每次执行 next-record 的函数,迭代器就向指定的方向前进,直到获取到所有的记录。

next-record 在 SQL 层面看上去是一个原子操作,但实际上整个扫描不是原子的。事务锁将阻止对迭代器访问到的记录的修改,但不知道对尚未交付的记录使用的并发控制的形式,具体的形式应该参考并发控制的方式。

Garbage Collection

latch-free 环境不允许独占访问共享的数据结构(例如,Bw-tree页面),这意味着一个或多个线程可能正在访问页面,即使页面正在更新。其中一种最糟糕的情况莫过于一个线程访问了正在被释放的数据页,这种情况发生在页面的压缩合并过程,一个页面即将被释放,但是另外一个线程的访问操作尚未结束,这是系统中垃圾回收常见的问题。只有当一个页面不再可能被任意的线程访问时,才能够安全地将其从内存中释放。在 Bw-tree 中,这种保证基于 epoch 机制实现。

epoch 机制是一种保护被释放的对象避免过早被释放重用的方法。当线程想要保护它正在使用的对象(例如,搜索时)不被回收时,它需要加入一个 epoch,epoch 的存在是全局可见的。只有在不再且不能再依赖任意共享数据对象时,线程才能退出这个 epoch。通常,线程在一个 epoch 中仅存在一个单个操作执行需要的时间。例如,在 epoch E中 enroll (登记)的线程可能已经看到了在epoch E中被释放的对象的早期版本(其创建开始于 epoch E 之前)。然而,在epoch E中登记的线程不可能看到在epoch E+1中被释放的对象,因为还没有进入其依赖区间。一旦在 epoch E 中登记的所有线程都完成并退出了这个 epoch,就可以安全地回收在 epoch E 中释放的对象了。使用 epoch 来保护执行存储和释放的 PID。在这个epoch 结束之前,这些对象是不能回收的。

BW-TREE STRUCTURE MODIFICATIONS

这部分主要讨论 Bw-tree 的结构性改变在无锁环境下是如何进行,包括节点的合并于分裂。

Node Split

Bw-tree 的节点的分裂过程同 B-Link tree 类似,当一个节点的键值数量超过一定限制,节点进入分裂阶段,分裂分为两个阶段,第一个阶段是对子节点的分裂,称为 half split;第二个阶段是将新节点插入父节点中。由于B-LInk tree 的节点包含对相邻节点的访问指针,因此这两阶段都是原子性的操作。

  • 子节点的分裂,首先由发起分裂的线程创建新的节点 Q,并在原先的节点 P 上找到合适的分裂点,Q 节点复制节点 P 上一半的数据以及向右的兄弟指针;然后,线程将向 Mapping Table 插入新的节点 Q 的相关信息。此时节点 Q 是仅对发起线程可见,而对其他线程则不可见。节点 P 和 节点 Q 的相连通过 CAS 进行,当成功将节点 P 向右的指针更新为节点 Q 时,half split 的第一阶段就完成了,这时节点 Q 对所有的线程就是可见的了。

  • 父节点索引更新时,会对父节点添加一个 index entry delta 的操作,表示对页面的 index 进行修改。当这个页面修改成功时,节点 Q 就可以被父节点查询到了。整个过程中可能会出现当前父节点处在另一个合并操作中的情况,epoch 机制可以保证对父节点的访问正常执行,这时对父节点的访问可能会上升到祖父节点的访问。

在这里插入图片描述

对页面的合并压缩(posting the delta)可以有效降低分裂节点和创建新节点的延迟(这应该是相较于普通 B±tree 的分裂)进而降低了节点分裂失败的可能性。

Node Merge

节点合并的情况和节点分裂的情况相对,当一个节点的数据量低于一定限度时就会触发节点和相邻节点的合并。相较于节点分裂更加复杂,需要更多的原子操作才能成功。

  • 节点删除,对一个页面添加删除操作 Move Node delta 表示这个页面即将被释放,不再进行服务。
  • 左侧节点的融合,需要对页面添加 node merge delta ,表示这个页面将和其右侧的兄弟节点进行融合,当执行合并压缩操作时将执行融合操作。
  • 父节点索引删除更新时,会对父节点添加一个 index term delete delta 的操作,表示对页面的 index 进行删除修改。当这个操作成功执行时,原页面 R 将无法在被访问,并被列入当前 epoch 的释放列表,当所有线程都退出本 epoch 时,页面被释放。

在这里插入图片描述

Serializing Structure Modifications and Updates

Bw-tree 的无锁设计要求在其内部,每一个线程的操作都能线性化的执行,对于任意的 SMO,都不应该出现未完成而被观察到的状态。但在这样一个无锁的结构中,Bw-tree 并不能阻止这一切的发生。因此当线程遇到了未提交或未完成的 SMO 操作时,应该先等待该 SMO 的完成,然后再执行自身的更新或者 SMO 操作。举例来说,在 Node Split 中,当一个更新线程观察到其将要操作的页面上正在发生分裂,需要等待父节点上 new index term delta 被执行(posting)才能继续自身的操作。

CACHE MANAGEMENT

Cache Layer 负责对 Mapping Table 中页面的维护和交换,内存中的数据在一定时间的积攒后,需要刷到磁盘中,例如事务中检查点的需求,就需要对部分存在内存中的数据进行刷盘,刷盘的操作同样涉及到并发保护,Bw-tree 通过对需要刷盘的页面添加 flush delta record 来保证。flush delta record 会记录之前刷盘的位置,使得每次刷盘操作只需要刷入增长的数据变量。

Write Ahead Log Protocol and LSNs

以 Bw-tree 作为底层,可以构建出具备事务功能的存储系统,这涉及了从 data component (DC) 到 transactional (TC) 的转变和控制,这中间需要添加组件来保证正确性,例如 Deuteronomy。

所有的更新操作需要以 Log Sequence Number (LSN) 进行标识,每一个 flush delta record 会保存当前最高的 LSN,标记当前 flush 的边界。

每当 TC 对日志数据进行追加,都会更新 End of Stable Log (ESL)的值,标识所有应该被存储在外存的数据边界,随后 DC 会执行对 ESL 之前的数据的刷盘。TC 需要使用 WAL 协议保证任何大于 ESL 的数据不会在 DC 持久化。

线程发起的 SMO 同样遵循 CAS,只有赢得了更新机会的线程才能提交更新并执行对应发起的 SMO,这个部分也被包含在对应的事务中。

Flushing Pages to the LSS

在对一个页面进行刷盘前,应该先完成对该页面的更新执行(posting delta)。

刷盘操作是增量数据的刷盘,而不是全量刷盘,每次刷盘的数据是未刷盘的最大 LSN 到当前 ESL 之间的数据。

刷新缓冲区将对 LSS 的写聚合到一个可配置的阈值(当前设置为1MB)以减少I/O开销(成组刷新)。它使用 ping-pong (double) buffers 双缓冲区,并在它们之间交替使用对 LSS 的异步I/O调用,以便可以在当前一批页刷新进行时为下一批页刷新准备缓冲区。当刷盘完成时,Mapping Table 中页面状态会进行更新。标识当前页面已经完成了刷盘。

Conclusion

写在后面,Bw-tree 在部分数据库系统中已经存在较为高效地实现,但是名称上似乎没人提起这与 Bw-tree 地关联,而是代称为 B±tree 的变种,两者之间相似度确实很高,但是在对数据的管理思想上却截然不同,这篇文章只是对比较基础版本的 Bw-tree 有一个简略地叙述,当存储结构具体当作一个数据库的底层存储结构时,还有许多的问题需要讨论,当然这篇文章也没有展开讨论这些内容,包括以 epoch 机制做的垃圾回收,似乎同许多语言设计的垃圾机制有许多共通之处,但是又有哪些区别,都没有深入地讨论,以及当 C++ 的特性不断出现,是否有些部分的设计可以借鉴 STL 的新特性,希望能从更多的文章中找到相关的答案。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值