B+树的Copy-on-Write设计

    本文主要介绍B+树的Copy-On-Write,包括由来、设计思路和核心源码实现(以Xapian源码为例)。中文的互联网世界里,对B树、B+树的科普介绍很丰富,但对它们在工业界的实际使用却几乎没有相关介绍文章,本文既是总结分享,也是资料索引。html

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

    在介绍CoW(Copy-on-Write)以前,首先思考这样一个问题:使用B+树的数据库在提供读、写服务时,若是叶子节点发生了节点分裂,而此时又有读行为,怎么保证读写的线程安全?譬如:准备读取叶子节点leaf时,leaf分裂为leaf和leaf-new两个block,这时候仍是读取leaf节点,不就可能致使数据丢失了吗,怎么解决的?github

    其中的一种解决方法即是CoW B+树(其它解决方法还有:B-link树、加锁后原地更新数据等等),这种方法,也有些文章(见文末参考文献)起了一个抽象层次更高的名字,叫作:shadowing。实现思路:在对数据进行操做(增、删、改)以前,先把全部可能操做到的层级(全部祖先节点)数据块都拷贝一份出来,后面的修改就在这份拷贝后的数据块上作修改,修改完以后再把这些数据块写入到磁盘文件新的位置上,这时候磁盘中就有两份数据,一份是修改以前的,一份是修改以后的,从修改以前的根节点开始遍历,能够读到全部修改以前的旧版数据,从修改以后的新根节点开始遍历,能够读到全部修改以后的新版数据。 从不一样的根节点进去能够读取到不一样版本的数据,这个CoW既保证了读写安全,也带有很优雅的数据备份功能(数据快照)。数据库

    举个实际的例子:在一个有7个节点(block)的B+树中,根节点为A,其叶子节点C有修改操做。把C以及它的祖先节点都拷贝一份:C'、B'、A',而后再在这些新拷贝的节点上修改数据,最后将修改后的数据写入到磁盘文件新的位置上。在这个过程当中,若是有业务在读取这颗B+树,仍然能够读取到C、B、A的完整旧数据。等到C'、B'、A'节点的数据刷写到磁盘完毕,再修改这颗B+树的根节点为A',这时业务就能读取到这颗B+树的新数据,此时旧数据A、B、C也仍然存在,能够选择保留做为备份,也能够选择回收磁盘空间。(参考了文末的文章《B-trees, Shadowing, and Clones》)api

B+树的CoW示例,每个框表明磁盘中的一个数据块(block) 安全

    原理部分介绍完了,很简单吧?并发

    咱们来看看Xapian是怎么实现CoW B+树的,由于这是一个大系统里的一部分代码,看不懂也不要紧,感兴趣的朋友也能够直接阅读完整源码。app

   首先是,在修改前的节点拷贝。源码:高并发

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中修改数据,这块包括了增、删、改,代码比较多,不贴。post

    最后,将全部的修改提交(commit),这里有顺序要求:一、将游标中全部未持久化的数据写入磁盘;二、在内存中让新根节点生效;三、新根节点以及其它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);
}

    以上是CoW B+tree的主要内容。

    有朋友可能会有疑问,我就添加几个字节的数据,采用CoW设计,须要拷贝多个数据块,是否太浪费了?确实浪费,因此采用CoW的系统通常都会攒一堆数据以后,再写入到B+树索引中,在尽可能保证时效性前提下减小拷贝新数据块、减小写磁盘;另外,对读多写少的业务场景来讲,写入时的性能浪费几乎能够忽略,而带来的收益倒是读取的高并发,是很是值得的trade-off。
    参考《A Short History of the BTree》,CoW B+tree是第三代技术。第四代技术围绕着CoW的更新效率、空间浪费、随机IO这几方面的缺点作了优化,做者称之为“‘stratified B-tree”,相关文章《Stratified B-trees and versioning dictionaries》,后续有时间再作学习。

学习资料:

一、《Xapian源码1.4.10

二、《how the append-only btree works》介绍如何实现append-only B-tree,很是详细易懂

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

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

相关文章
相关标签/搜索