-- 什么?B树?

-- 不好意思,我不仅有B树,我还有……w

emmm,虽然 B 树已经提供了相当优秀的使用体验了,但是如果要真的使用它来做文件系统,我们还需要一些小小的改动w

这篇博客将介绍一部分的这些改动,下一篇博客就要用 btrfs 作为例子来介绍 B 树在文件系统里的应用了w

(啊……我就在用 btrfs w…

0x00 B+ trees

B+ trees example

Author: Grundprinzip
From: Wikipedia B+ tree
License: Creative Commons Attribution 3.0 Unported

B+ tree 是 B 树的一种变种,它使用一种不同的模式来存储键值对:在非叶子节点中,仅存储键,在叶子节点中,存储键和指向值的连接(参考上图)。

由于这种设计模式,B+ 树的根节点要么是叶子节点,要么是有两个或者以上键的非叶子节点。

首先定义 b(branching factor) 是 B+ 树的一个节点最大拥有的子节点的数量,那么:

非根节点的最小键数量是上取整(b/2)-1 (最大键数量当然是……)。根节点和 B 树一样不受限制。

B+ 树的插入、删除、查找算法都与 B 树没有较大的区别。但是有一点需要注意的是,B+ 树为了保证能够实现常数时间复杂度的前驱后继(在文件系统实现当中,这是一个非常重要的特性),所以将叶子节点的最后一个指针指向了它的紧邻的右子节点,这让整个 B+ 树的叶子节点的值形成了有序的链表。在执行操作时,务必对这些指针进行维护。

为了解决 B 树的并行访问问题(写时读,一个成熟的文件系统不可能在写的时候不允许读取),B-link 树被设计了出来 ( Efficient Looking for Concurrent Operations on B-Trees, 1981 )。

B 树没有办法在写的时候读取的问题在于写入可能会导致树平衡,从而修改树的结构。

B-link 树采用了增加一个指针来解决这个问题。它在所有 B 树原来的节点上增加了一个指针,这个指针指向了它紧邻的右节点。在树结构变换时,会首先保证这个指针被更新。在这种情况下,由树结构改变导致的节点无法访问可以通过指针来访问到。

接下来将简要介绍 B-link 树的算法。由于增加了指针,它的一些算法和一般的 B 树略有区别w

  1. 按照 B 树的方式找到它应该在的叶子节点。
  2. 如果它在那个节点,那么就查找到了
  3. 如果它不在,用指针向右遍历。

Insertion

注意所有修改节点和节点关系的操作都是需要加锁的。

关键的区别产生在将节点拆分的过程。

  1. 首先创建拆分后的右子节点,将数据复制进去,并将它的后继指针指向原先的后继。
  2. 将原先节点的后继指向新的右子节点,然后将应该复制进去的数据删除。
  3. 最后将这个右子节点连接到父节点上。如果这个连接过程使得父节点需要拆分,那么递归向上。

注意在拆分父节点的过程中,需要向右遍历:b-link 树的结构不适合维护指向父节点的指针,因此在更新某一子节点的过程中,父节点被并行的其它操作更新了。

Deletion

Emmmm… B-link 树使用了一种……比较emmm的方式解决删除的并行化w

那就是仅删除键,不平衡树……

0x02 Shadowing & Cloning based on B+-trees

B-trees, Shadowing, and Clones ,Ohad Rodeh, 2007

emmmm…

这篇论文说的是基于 B+ 树的 Shadowing 和 Cloning 实现(emmm 我不太清楚中文翻译过来是什么),但是实际上为了保证不会过分地 Shadow 节点,将不维护指向紧邻右节点的指针。

Shadowing

什么是 Shadowing 呢?

Shadowing 就是在要修改某一个硬盘上的页(可能是存储树节点的、文件内容的、或者其他东西的区块)的时候,首先将它复制到另外一个地址,进行修改,再把指向原地址的指针改为指向新地址。

这个机制可以有效保证在突然崩溃的情况下对文件系统的操作不会损坏原有的文件系统结构。但是它在应用到带有指向紧邻右节点的指针的 B 树的时候,会导致不得不对整棵树进行 Shadow 。如果不维护这个指针,那么需要被 Shadow 的仅是从根节点到被修改节点的一条路径。

Concurrency…?

那么既然不维护指向紧邻右节点的指针,那么它要如何保证并行访问的性能呢?

答案是使用一种更为积极的操作策略w

相比传统的 B 树实现(在完成插入/删除操作后再进行节点的拆分/合并),为了达到较好的并行访问效率,这种结构将在寻找将要插入/删除的位置的过程中对遇到的达到上/下限的节点立即进行拆分/合并。

这样可以保证对一个写入操作,同时需要加锁的节点最多只有一个节点和它的直接子节点。

Locking Scheme

在整个操作过程中,加解锁是按照以下策略进行的:

  • 不允许写时读
  • 不允许同时写
  • 允许同时读

(也就是说,读写两种锁之间互斥,写锁之间互斥,读锁之间共享。

Clones

Cloning 指的是快速地对整个文件系统进行快照的能力。这个特性的实现旨在能够快速地备份文件系统在某个时刻的状态,并且在将来能够只读地回溯到这个状态,或者将整个文件系统重置到当时的状态。

Cloning 的可用性取决于以下四个方面:

  • 空间效率:快照应当通过与当前文件系统尽可能地共享页面来节省空间消耗。
  • 速度:创建快照应当尽可能地快。
  • 快照数量:快照数量应当尽可能地多。
  • 操作自身:快照应当能够快照某个快照。

为了达到以上这些特性,我们采用一种基于“引用计数”的方式。

基本的思想是使用一个空闲空间表来存储对硬盘上每一块空间的引用次数。当被 clone 的时候,那个页面的引用次数就 +1 。 当某一块空间将被修改时,如果它的引用次数大于 1 ,那么它将被复制一份。

具体的算法如下:

Create a Clone

  • 复制根节点
  • 将根节点的直接子节点的引用计数 +1 。

Modify Keys

将要修改一个节点时:

  • 如果引用次数 > 1,那么将它复制,并且将它的子节点的引用计数 + 1。

通过这样的方式,使得创建 Clone 消耗的时间非常小,而且能够最大可能地利用公共的页面。