您的位置:首页 > 运维架构

B+树的Copy-On-Write设计

2019-04-14 22:58 671 查看

    本文主要介绍B+树的Copy-On-Write,包括由来、设计思路和核心源码实现(以Xapian源码为例)。中文的互联网世界里,对B树、B+树的科普介绍很丰富,但对它们在工业界的实际使用却几乎没有相关文章,本文以及后续的一些分享,期望能补上这块的缺漏,这也是我个人的总结备忘。

    在阅读本文之前需要先对B+树有概念上的认识,可以阅读wiki,也可以看看这两篇简单易懂的中文漫画解读,B-treeB+tree

    在介绍COW(Copy-On-Write)之前,首先思考这样一个问题:当以B+树为底层磁盘数据结构的数据库在同时提供读、写服务时,如果叶子节点发生了节点分裂,而此时又有读行为,怎么保证读写的线程安全?譬如:准备读取叶子节点leaf时,leaf分裂为leaf和leaf-new两个block,这时候还是读取leaf节点,不就可能导致数据丢失了吗,怎么解决的?

    解决方法便是B+树的COW,也有些文章起了一个抽象层次更高的名字,叫做:shadowing。实现思路:在对数据进行操作(增、删、改)之前,先把所有可能操作到的层级(所有祖先节点)数据块都拷贝一份出来,后面的修改就在这份拷贝后的数据块上做修改,修改完之后再写入到磁盘文件中,这时候磁盘中就有两份数据,一份是修改之前的,一份是修改之后的,从修改之前的根节点开始遍历,可以读到所有修改之前的旧版数据,从修改之后的新根节点开始遍历,可以读到所有修改之后的新版数据。 从不同的根节点进去可以读取到不同版本的数据,这个COW既保证了读写安全,也带有数据热备份功能。

    举个实际的例子:在一个有7个节点(block)的B+树中,根节点为A,其叶子节点C有修改操作。把C以及它的祖先节点都拷贝一份:C'、B'、A',然后再在这些新拷贝的节点上修改数据,最后将修改后的数据写入磁盘。在这个过程中,如果有业务在读取这颗B+树,仍然可以读取到C、B、A的完整旧数据。等到C'、B'、A'节点的数据刷写到磁盘完毕,再修改这颗B+树的根节点为A',这时业务就能读取到这颗B+树的新数据。(参考了文末的文章《B-trees, Shadowing, and Clones》)

B+树的COW示例,每一个框代表磁盘中的一个数据块(block)

 

    原理部分介绍完毕,我们来看看Xapian是怎么实现COW B+树的。

   首先是,在修改前的节点拷贝。

void GlassTable::alter() {
LOGCALL_VOID(DB, "GlassTable::alter", NO_ARGS);
Assert(writable);
if (flags & Xapian::DB_DANGEROUS) {
C[0].rewrite = true;
return;
}
int j = 0;
while (true) {
if (C[j].rewrite) return; /* all new, so return */
C[j].rewrite = true;

glass_revision_number_t rev = REVISION(C[j].get_p());
if (rev == revision_number + 1) {
return;
}
Assert(rev < revision_number + 1);
uint4 n = C[j].get_n();
free_list.mark_block_unused(this, block_size, n);   /// 将当前需要被拷贝的block设置为空闲,这个空闲标记要等到新block被刷到磁盘(commit操作)之后才生效
SET_REVISION(C[j].get_modifiable_p(block_size), revision_number + 1);  /// j层级的游标申请新的内存block,并设置版本号+1
n = free_list.get_block(this, block_size);  /// 从空闲块中取一个块号作为新block的块序号(block_size*n也即是这个块在磁盘文件的偏移)
C[j].set_n(n);  /// 将块序号设置到游标中

if (j == level) return;   /// 如果根节点也已经拷贝完毕,则返回
j++;  /// j+1,准备拷贝父节点
BItem_wr(C[j].get_modifiable_p(block_size), C[j].c).set_block_given_by(n);  /// 修改当前(j-1)层级的父节点指向新的block,注意:这里修改的也是拷贝后的节点数据
}
}

    然后,在新block中修改数据,这块包括了增、删、改,代码比较多,不贴。

    最后,将所有的修改提交(commit),这里有顺序要求:1、将游标中所有未持久化的数据写入磁盘;2、在内存中让新根节点生效;3、新根节点以及其它meta信息写入版本文件(也就是记录B+树元信息的文件:iamglass文件)。

    部分源码:

void GlassDatabase::apply() {
LOGCALL_VOID(DB, "GlassDatabase::apply", NO_ARGS);
if (!postlist_table.is_modified() &&
!position_table.is_modified() &&
!termlist_table.is_modified() &&
!value_manager.is_modified() &&
!synonym_table.is_modified() &&
!spelling_table.is_modified() &&
!docdata_table.is_modified()) {
return;
}

glass_revision_number_t new_revision = get_next_revision_number();

int flags = postlist_table.get_flags();
try {
set_revision_number(flags, new_revision);  /// 这里做了数据写入磁盘、生效新节点数据的操作
} catch (const Xapian::Error &e) {
modifications_failed(new_revision, e.get_description());
throw;
} catch (...) {
modifications_failed(new_revision, "Unknown error");
throw;
}
/// 下面这一票代码,是为了记录修改到changeset文件,changeset文件用于主从节点的增量数据同步
GlassChanges * p;
p = changes.start(new_revision, new_revision + 1, flags);
version_file.set_changes(p);
postlist_table.set_changes(p);
position_table.set_changes(p);
termlist_table.set_changes(p);
synonym_table.set_changes(p);
spelling_table.set_changes(p);
docdata_table.set_changes(p);
}

 

参考:

1、《Xapian源码1.4.10

2、《how the append-only btree works》介绍如何实现append-only B-tree,非常详细易懂

3、《A Short History of the BTree》B-tree历史介绍

4、《B-tree, Shadowing, and Clones》 很详细的B+树 Shadowing、COW介绍文章

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: