- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
原文链接: https://mp.weixin.qq.com/s/I5TphQP__tHn6JoPcP--_w 参考文献不一定能下载。如果你获取不到这几篇论文,可以关注公众号 IT技术小密圈 回复 bw-tree 获取.
Bw-Tree 希望实现以下能力
缓存层中维护着 映射表(mapping table ), 保存逻辑页和物理页的映射关系,逻辑页由逻辑页标识符 PID 唯一标识.
映射表将 PID 映射为以下两种地址之一
BW-Tree 的节点指针都是逻辑的 PID,因此在 SMO 操作过程中, 某些节点的物理地址发生变化,并不需要更新所有对该节点有引用的所有节点指针(PID 并没有发生变化).
BW-Tree 通过创建描述变更内容的 增量记录(delta record) 并将其插入到当前页的前面来实现对页的状态变更.
如下图 (a) 中,先将对 Page P 的一次变更操作做成一个增量记录 ∆D ,并让 ∆D 指向 Page P。然后将 Page P 的逻辑地址 PID P 映射的物理地址通过 CAS(compare and swap) 原子操作由 Page P 的物理地址改为 ∆D 的物理地址。(Page P 被称为 Base 页) 。
当变更导致前置的增量记录达到一定的规模之后,会触发合并操作,将所有的增量记录和原本的页合成一个新的页.
如下图(b) 中,将 Page P 前置的所有增量记录和 Page P 一起合并为一个 Consolidated Page P , 然后通过 CAS 操作将 Page P 的逻辑地址 PID P 映射的物理地址替换为 Consolidated Page P 的物理地址。Page P 及其前置的所有增量记录将会被垃圾回收机制回收处理.
BW-Tree 在闪存中的存储结构如下图。当增量记录( ∆record )达到一定数量之后,会执行一次刷盘操作将所有 Base 页的增量记录一起顺序写入磁盘.
这将会导致每一个 Base 页和它对应的许多 ∆record 并不在相邻的地址内,而闪存的随机读性能和顺序读性能几乎一致,因此可以接受。(如果是其他顺序读性能更好的持久化存储可能需要一定优化,后文有提及。) 。
如上文所述, 并不是所有的变更操作都立即刷盘(而是会等待增量记录达到一定数量规模才会一次刷盘)。因此,在每次执行变更前,记录 WAL 日志也是必要的.
给每一次变更操作一个日志序列号(LSN), 当某次刷盘完成之后, 对应的最新 LSN 之前的 WAL 日志都可以失效.
如上图所示, BW-Tree 的每个节点都有唯一的逻辑地址 PID(N1, N2, ..., Ni, ..., Nj, ..., Nk, ...) 。节点之间不使用物理地址,而是使用逻辑地址 PID 相互引用.
当需要获取某个节点的物理地址时,会先查询映射表,将 PID 转化为物理地址。 因此在对单个原子的 CAS 指令就能实现对有多个引用的节点的物理地址进行变更.
BW-Tree 和其他基于 B+tree 索引直接最大的不同在于 BW-Tree 避免直接操作树的节点,而是直接将节点的增删改查保存增量记录中,这样极大地减少了 CPU 缓存失效的概率.
另外将每个 Base Page 的变更维护在一条 增量链(Delta Chain) 中,并通过中间层映射表隔离 Page 地址的变更(PID 保持不变), 使得可以在一次原子 CAS 中实现对 Page 进行变更操作.
如下图所示,在 BW-Tree 中,一个逻辑节点包含两部分: Base 节点和 Delta 链。Base 节点记录当前节点的在上一次合并(consolidate) 之后的数据,Delta 链记录在此之后 Base 节点发生的所有变更操作.
Delta 链将对 Base 节点的操作按照时间顺序用单向链表(物理指针)连接起来,链表的结尾处指向 Base 节点.
Base 节点和 Delta 链中的每一条 Delta 记录都保存了一些额外的元数据信息,它标识逻辑节点在某次操作时的状态(每次对某个节点做变更操作,都会将最新的状态记录在最新的 delta 记录中)。这些信息将会用于树的遍历等操作.
下表解释了这些元数据的内容.
[low-key, high-key)
。如上图中,逻辑节点的数据范围始终未变,在每个 Delta 记录及 Base 节点中都是 [K1, K8)
。 N8
。 5
;在执行完 ∆delete [K1, V1]
操作后,size 变为了 4
;在执行完 ∆insert [K2, V2]
操作后,size 又变为了 5
。 ∆delete [K1, V1]
操作的 depth 为 1
, ∆insert [K2, V2]
操作的 depth 为 2
。 ∆delete [K1, V1]
操作中, K1
在 Base 节点的第一位,因此它的 offset 为 0
。 ∆insert [K2, V2]
操作中, K2
在 Base 节点的第二位,因此它的 offset 为 1
。
BW-Tree 的所有 SMO 操作都是通过原子操作实现的 latch-free 操作, 它将单个的 SMO 操作拆分为一些列 CAS 原子操作。 为了确保没有线程需要等待其他线程的 SMO 操作结束,当它发现部分完成的 SMO 操作时,会在执行当前线程原本的任务之前,先将部分完成的 SMO 操作剩下部分执行完成。(help-along protocol) 。
下面本文将会详细介绍 BW-Tree 具体是如何实现这样的能力的.
与 B-link tree 类似, BW-Tree 将 split 分为两个阶段: 先将子节点用原子操作拆分为两个节点( half split ), 然后将新的 分隔键(separator key) 和刚拆分的子节点的指针用原子操作更新到其父节点.
以上图将 O 节点的子节点 P 拆分为 P 节点和 Q 节点为例
拆分子节点(half-split) (a) 创建 P 节点的兄弟节点 Q : 如上图 (a) 所示。申请一个新的 Page 作为 Q 节点;在节点 P 中找一个合适的键 Kp 作为节点 P,Q 的分隔键(separator key).
节点 P 仅保留小于 Kp 的数据,大于等于 Kp 的数据将拷贝到节点 Q;将节点 Q 的兄弟节点设为节点 R(即当前节点 P 的兄弟节点);将节点 Q 注册到地址映射表中.
整个流程中,节点 Q 均不被用户可见,因此不需要原子操作。在这个阶段节点 P 依然处于为分裂状态.
(b) 更新 P 节点, 将 Q 节点作为其兄弟节点 :如上图 (b) 所示。为节点 P 创建执行分裂操作的 delta 记录( Split ∆ ), 该记录包含两个信息: 将 Kp 作为节点 P,Q 的分隔键以及让 Q 节点作为 P 节点的兄弟节点(让 P 逻辑节点的兄弟节点指针 right-sibling 指向 Q 节点的逻辑地址 Q); 然后调用 CAS 原子操作将逻辑地址 P 指向 Split ∆ 的地址.
当 CAS 操作完成时,对节点 P, Q 的所有查询,都将会被父节点 O 路由到 P 逻辑节点(Split ∆)。如果待查询的 K 小于 Kp, 查询将会被路由到节点 P。若 K 大于等于 Kp, 查询将会通过 right-sibling 路由到节点 Q.
更新父节点 :要实现直接从父节点 O 路由到刚被分裂的节点 Q(而不经过节点 P),需要将节点 Q 的信息更新到节点 O 中。如上图 (c) 所示。 先创建一个指向节点 O 的 Delta 记录 Index entry ∆ ,它包含了三个信息: (a) 节点 P, Q 的分隔键 Kp; (b) 指向节点 Q 的逻辑地址;(c) 节点 Q 和其 right-sibling 的分隔键 Kq(Kp 和 Kq 确定出节点 Q Key 的范围 [Kp, Kq) ).
如上图所示,当某个节点的大小小于某个阈值,BW-Tree 将使用 latch-free 的方式将它合并到其他节点(BW-Tree 仅支持与左兄弟节点合并).
以上图将 P 节点的子节点 R 合并到节点 L 为例:
将 R 节点标记为删除 : 如上图 (a) 所示。为节点 R 新增 Delta 记录 Remove Node ∆ , 用于将逻辑节点标记为删除。当查询访问到 Remove Node ∆ 节点,将会跳转到节点 R 左边的兄弟节点,即节点 L.
合并子节点 : 如上图 (b) 所示。为节点 L 新增 Delta 记录 Merge ∆ ,该记录将节点 L 与节点 R 合并起来作为一个逻辑节点整体.
在步骤 1 到步骤 2 之间,实际上是无法感知到节点 R 的。(因为节点 R 已经被Remove Node ∆ 节点逻辑移除了 )。在步骤 2 执行之后, 才能通过 Remove Node ∆ 跳转到 R 的左兄弟节点 L, 通过 Merge ∆ 查询到节点 R 的值.
但是这并不会影响并发操作的正确性,因为 help-along protocol 会保证在发现其他线程存在未完成 SMO 操作的情况下,先将 SMO 操作执行完成,再进行原本的操作。因此就不会在步骤 1 到步骤 2 之间去对节点 R 进行操作.
更新父节点 : 如上图 (a) 所示。父节点添加 Delta 记录 ∆ Delete Index Term for R ,用于将节点 R 在父节点中的索引删除。节点 L 将节点 R 的索引范围也纳入其中.
在这个阶段之后,Remove Node ∆ 这个 delta record 和节点 R 在地址映射表中的位置都将不再被使用, 他们将会被 epoch GC 逻辑回收.
唯一键查询 : 唯一建的查询和普通的 B+ 树类似,唯一的区别在于,当遍历到叶节点时,如果存在 Delta 链,它会先依次遍历 Delta 链,并将最先出现的结果返回。当 Delta 链中不存在时,才会去 Base 节点执行二分查找.
非唯一建查询 :当定位到数据仅可能存在在某个叶子节点时,必须遍历所有的 Delta 链和 Base 节点才能查找出指定键的所有值。操作逻辑如下图.
在遍历 Delta 链的过程中,将已知符合要求的数据放在集合 Spresent , 将已知被删除的数据放在集合 Sdeleted 。按顺序遍历 Delta 链时,当遍历到插入 Delta 记录(K, V) 时,如果 V 不在 Sdeleted,则将其加入 Spresent。当遍历到删除 Delta 记录(K, V) 时,如果 V 不在 Spresent,则将其加入 Sdeleted.
记 Sbase 为 Base 节点中的该键的所有值的集合。 则最终的查询结果为 Spresent ∪ (Sbase - Sdeleted) 。
正向 Scan : 正向遍历会将正在处理的节点拷贝到迭代器中。当迭代器中保存的节点的数据全部遍历完成,就会继续将下一个节点的数据全部拷贝到迭代器继续遍历。 因此,整个 Scan 过程读取的数据并不是一个快照(snapshot)的数据 .
如下图所示,当遍历完一个节点 N0 [K0, K1) 的数据,会查找该节点的上界 K1 所在的节点作为下一个节点。 如果遍历 N0 过程中, N0 发生的 SMO 操作是的 N0 键的范围变大, 该节点的上界 K1 所在的节点依然是 N0, 则将新的 N0 拷贝到迭代器中。然后查找到 K1 的位置,继续遍历该节点.
反向 Scan :反向 Scan 整体逻辑和正向 Scan 一致。唯一的区别在于反向 Scan 的下一个节点的查找方式有所不同。反向 Scan 遍历完一个叶子节点后,会将小于该叶子节点的下界的最大的 Key所在的叶子节点作为下一个遍历的节点.
如下图,N1 的下界是 K5, 小于 K5 的最大键为 K4(N0), 因此, K4 所在的节点 N0 就是就是 N1 遍历完之后,下一个需要遍历的节点.
中心化 Epoch GC : 如下图 (a) 所示, 唯一的 GC 线程(Background Thread) 维护 Epoch 链表。每个 Epoch 节点维护引用当前 Epoch 删除的资源及其引用线程数的总和。当某个 Epoch 的线程引用计数恢复 0 时,该 Epoch 及其维护的垃圾资源可以被删除。如下图中的 Epoch 101.
去中心化 Epoch GC : (1) 全局 Global Epoch 维护全局 Epoch 时钟 e_global。每个工作线程产生的垃圾节点由本线程维护在本地垃圾回收链表 l_local, 并将该垃圾节点的 e_delete 设置为当前进程的 e_local.
(2) 当某个线程开始索引操作时,会先将当前的全局 Epoch 时钟 e_global 拷贝到当前线程,记作 e_local。当该索引操作结束后,会再次将 e_local 刷新为 e_global。 (3) 每个工作现场会定期获取当前全局最小的 e_local, 并将本线程维护的 l_local 中 e_delete 小于全局最小 e_local 的垃圾节点回收.
[start, end)
. 依次遍历 Delta 链,将键区间分为多个部分。 [s, e)
拆分为 [s, offset)
和 [offset, e)
。 [s, e)
拆分为 [s, offset)
和 [offset+1, e)
。
当工作线程遍历 Delta 链查找键 K 时, 它会初始化二分查找的偏移量(offset) [min. max) 范围为 [0, +inf) 。遍历过程中,当遇到键位 K', 偏移量为 offset 的 ∆insert 或 ∆delete 记录,它会比较 K 与 K'。若 K=K', 则立即得到 K 所在偏移量为 `[offset, offset+1)``,不用在 Base 节点进行二分查找。若 offset > min 并且 K>K′, 则将 min 设为 offset。 若 offset < max 并且 K <K′, 将 max 设为 offset。如果最后的区间大小大于 1, 则在偏移量区间内二分查找键 K.
如下图中的例子, 最终得到的区间是 [2, 5) , 因此最后只需要在 Base 节点中 offset 在 [2. 5) 区间内的键二分查找 Key=6.
本文更多的是介绍内存内的 BW-Tree 的维护逻辑,更多关于持久化数据的维护相关的内容请查看 LLAMA: A Cache/Storage Subsystem for Modern Hardware 。后续我也会在公众号 IT技术小密圈 更新对该论文的分享,欢迎关注.
参考文献可能不太好下载。如果你获取不到这几篇论文,可以关注公众号 IT技术小密圈 回复 bw-tree 获取.
最后此篇关于【技术分享】万字长文图文并茂读懂高性能无锁“B-Tree改”:Bw-Tree的文章就讲到这里了,如果你想了解更多关于【技术分享】万字长文图文并茂读懂高性能无锁“B-Tree改”:Bw-Tree的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有两种结构,Header 和Session,它们都符合协议(protocol)TimelineItem。 我有一个 Array 由 TimelineItem 组成,如下所示: [Header1, S
这个问题在这里已经有了答案: Multiple assignment and evaluation order in Python (11 个答案) 关闭 6 年前。 我刚接触python所以想问你
我试图找到一种方法来在 R 中获取 A、A、A、A、B、B、B、B、B 的所有可能的唯一排列的列表。 组合最初被认为是获得解决方案的方法,因此组合的答案。 最佳答案 我认为这就是你所追求的。 @bil
我怎样才能将两个给定的向量混合成一个新的向量,它以交替的顺序保存它们的值。 (f [a a] [b b]) ; > [a b a b] 这是我想到的: (flatten (map vector [:a
这是我的第一个问题,我开始学习Python。之间有区别吗: a, b = b, a + b 和 a = b b = a + b 当您在下面的示例中编写它时,它会显示不同的结果。 def fib(n):
这个问题在这里已经有了答案: Why is there an injected class name? (1 个回答) 12 个月前关闭。 我不知道如何解释: namespace A { struct
我尝试了一些代码来交换 Java 中的两个整数,而不使用第三个变量,使用 XOR。 这是我尝试过的两个交换函数: package lang.numeric; public class SwapVars
假设类 B 扩展类 A,并且我想为 B 声明一个变量。什么更有效?为什么? B b或 A b . 最佳答案 您混淆了两个不同的概念。 class B extends A { } 意味着B 是 A .
我不确定这个问题的标题是什么,这也可能是一个重复的问题。所以请相应地指导。 我是 python 编程的新手。我有这个简单的代码来生成斐波那契数列。 1: def fibo(n): 2: a =
我在谷歌上搜索了有关 dynamic_cast 的内容,我发现显式地将基类对象转换为派生类指针可能是不安全的。但是当我运行一些示例代码来检查它时,我没有收到任何错误。请在下面找到我的代码: class
这个问题在这里已经有了答案: What is this weird colon-member (" : ") syntax in the constructor? (14 个答案) 关闭 8 年前。
在不重现产生非整数值的表达式的情况下实现以下目标的惯用方法是什么(在我的真实情况下,该值是在我不想重现的冗长查询之后计算为百分比的): SELECT * FROM SomeTable WHERE 1/
在析构中,这两个代码的结果确实不同。我不确定为什么。 提示说 const [b,a] = [a,b] 将导致 a,b 的值为 undefined (从左到右的简单分配规则)。我不明白为什么会这样。 l
C++ Templates - The Complete Guide, 2nd Edition介绍max模板: template T max (T a, T b) { // if b < a th
我最近开始学习代码(Java),并根据第 15.17.3 节在 Oracle 网站上查找了模运算符。以下链接: http://docs.oracle.com/javase/specs/jls/se8/
无法理解以下行为。 d1 := &data{1}; 的区别d1 和 d2 := 数据{1}; &d1。两者都是指针,对吧?但他们的行为不同。这里发生了什么 package main import "f
这个问题在这里已经有了答案: How to make loop infinite with "x = y && x != y"? (4 个回答) How can i define variables
在我的程序中,当我调试我的代码时,它似乎在我生成的代码中的某处 X1=['[a,a,a]','[b,b,b]'] 还有我生成的其他地方 X2=[[a,a,a],[b,b,b]] 当我想添加这两个列表然
我试图使用递归将两个整数相乘,并意外编写了这段代码: //the original version int multiply(int a, int b) { if ( !b ) retu
我有一个列表中数字之间所有可能的操作组合: list = ['2','7','8'] 7+8*2 8+7*2 2*8+7 2+8*7 2-8*7 8-2/7 etc 我想知道是否可以说像 ('7*2+
我是一名优秀的程序员,十分优秀!