读后感:The Bw-Tree: A B-tree for New Hardware Platforms

本文继续论文读后感序列。读后感并不全文翻译,重点关心的是问题、思路、关键细节(当然对关键的理解因人而异)以及测试数据,欢迎交流。

一、问题

新的硬件的出现促使人们重新思考数据管理系统应该如何演进。文中描述的新的硬件趋势主要是固态硬盘以及多核CPU。

固态硬盘提供了超高的IOPS以及极低的读写延时,如果上层的系统能够充分利用这个特性,能够大幅的提高性能。同时,固态硬盘的不对等特性(读比写快,顺序写比随机写快)也要求上层系统能够做出适应性的设计。传统的B+-Tree对page的更新会引起非常多的随机写,很难有效地利用固态硬盘的特征。

单核CPU的频率提升已经接近极限,多核CPU顺势推出。跟单核相比,充分利用多核CPU面临的最大问题是并发能力,而典型情况下锁是限制并发的最大问题。其次cache 命中率对性能影响也非常大,毕竟CPU cache一般是SRAM,比起内存的DRAM来说,性能有数量级的优势。文中认为传统B+-Tree这两个问题都解决的不够好,更新相同页面要互斥,页面分裂向根节点传递,进一步增加了互斥的概率,in-place的更新破坏了CPU cache有效性,这些问题需要得到解决。

从问题的描述能够看到,本文提出的Bw-Tree主要场景是固态硬盘(Thus the Bw-tree targets flash storage.),甚至是全内存系统( The Bw-tree only blocks when it needs to fetch a page from stable storage (the LSS), which is rare with a large main memory cache. 最后的测试也是全内存测试),虽然文中也提到系统中的某些策略对磁盘也适用,但是通篇来看,不是重点。

二、思路

顺着上面的问题,作者提出的解决办法有如下几条。

  1. 使用mapping table增加page数据管理的灵活性:如果我们把B+-Tree最下面的一层看做数据层,而上面的各层看做索引层,那么这种操作做到了将索引和数据使用两个子系统来分开管理,这样两个子系统可以分别优化,索引子系统优化的目标是高并发,高cache命中率,数据子系统优化的目标是充分利用随机读和顺序写的优势,减少随机写。mapping table是两个子系统的连接点,一个关键组件。
  2. 使用delta+base代替in-place更新:base可以理解为某节点的第一次出现,delta可以理解为对base相应的修改(insert/update/delete),类似数据库redo。在更新Bw-tree节点的时候,在节点前面放一个delta,然后delta指向base,而不是直接修改base,这样同时达到了两个好处。一是避免多线程修改同一个页面的时候锁base降低并发度,二是因为base并未被修改,也不会破坏CPU cache有效性(如果base在cache里面的话)
  3. 通过一系列的原子操作实现分裂和合并:传统观念上,B+-Tree的分裂和合并不加锁是不可思议的,比如分裂场景,要从父页面删除一条指向子页面的记录,然后添加两条新的记录指向新分裂的两个页面(或者修改记录,再添加新记录)。如果不加锁,比如刚刚删除一条记录,另外的线程就来访问,结果就错了。文中处理此类多步骤原子性的问题有一个统一的思路,一是增加临时节点,为访问待更新节点建立冗余路径,然后CAS断老路接新路;二是在整个操作没有完成之前,任何试图访问受该操作影响的页面的线程都要拿过接力棒继续完成整个操作,然后才能继续。第二步实质上是一个等待,但是内存数据库中,碰到自己动手比等待其他人完成后通知更快。
  4. 只写delta让存储更高效:文中提到其Log Structure Store(LSS)只写delta非常高效,但是细节极少,其中一个细节个人觉得有点意思,The begin and end system transaction records are among the very few objects flushed to the LSS that are not instances of pages,看起来好像是用户事务和系统事务是不同的实例。

三、关键细节

《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》
《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》

首先看看系统架构。系统分三层,上面是Bw-tree索引层,最下面记录的不是数据,而是page id(文中没说page id怎么生成的);Cache层的核心作用是根据索引层的page id找到内存地址(有时候会根据page的flash offset去磁盘读,然后返回内存地址);再下面就是LSS,专门优化存储。直观看过去,跟传统B+-tree + buffer pool最大的不同就是多了个mapping table,虽然这种indirection不是作者的发明,但是这种映射确实带来了几个好处。第一个好处是如果page的位置变了,只要在mapping table修改就可以了,不用修改上面的Bw-tree索引(因为Bw-tree记录的是page id,这个没变),减少修改就减少了竞争。第二个好处是,mapping table对page 大小没有那么严格的要求(不用到了xK就分裂合并之类的,可大可小,业务确定),减少了分裂合并的次数,这就进一步减少了索引的修改。第三个是分离了内存和磁盘结构,使得各自都可以有更优的实现。

《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》
《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》

再来看delta update。初始状态是page id指向base page。对页面的更改不直接修改这个页面,而是构造一个delta,然后page id指向delta,delta指向base page。这个有点像是无锁链表里面插入一个节点,只不过这个节点跟后面的节点不是对等关系,而是一种类似合作的关系。在读某一个key的时候,如果读线程碰到delta,先在delta里面找是否有这个key,如果找到了就直接返回,否则就继续,一直找到base。这种方法减少了冲突,但是很显然带来了读放大的问题,本来读只需要搜索base page,现在要搜索所有的delta,某种程度上降低了读的性能(热点更新可能会更严重),此时就需要一个类似整理的过程,将这些delta和base整理成一个新的page换掉原来的base page,整理之后,读线程可以只搜索base page,读性能又恢复了。周而复始,这个模式很多系统都用,是一个读写平衡的问题。

《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》
《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》

最后,本文着墨最多的部分来了,如何在做页面分裂合并(structure modification operations , SMOs)的时候也能做到latch-free?上图描述了如何将页面P分裂成P和Q。首先,在mapping table为Q建立一个表项,Q指向P的邻居R,此时除了当前正在操作的线程没人知道Q的存在,其余访问该干啥干啥,没有任何影响。第二步,此时为页面P建立了一个影子代理,这个代理啥都知道,包括P即将被分裂为P和Q,P的分裂点,P的地址和Q的地址,然后通过CAS让mapping table里面的page id of P指向影子代理,这是一个原子操作,如果多个人同时操作只会有一个人成功(论文描述,任何线程都可能发起对P的分裂,所以这里是可能发生竞争的,如果P很热?)。此时,父节点和左邻居节点也一并指向了影子代理(因为他们都是通过page id访问的,也就是图中说的logical pointer,page id的指针已经指向影子代理)。到此为止,所有的访问,无论直接从mapping table里面通过page id访问P,还是通过邻居,通过父节点,对P的访问都是通过P的影子代理,因为影子代理拥有P所有的信息,所以各个访问都没有任何影响。唯一的问题是,如果父节点访问Q,需要先经过P的影子代理,这是有性能损失的,这是下面要解决的问题,即为父节点创建一个影子代理,确保影子代理能够直接识别Q。此时Q算是真正加入了大家族,后面就可以做一些整理的工作了,重点是各个节点和其影子代理的合并(此时节点唯一的入口就是影子代理,所以没有竞争问题)。

页面合并过程类似,核心思路就是先建立一个影子代理,当这个代理接管所有的对原节点的访问之后,原节点就可以被合并下线了。这里的想法很巧妙,mapping table使得一个节点唯一的物理引用就是来自表项,因此只要能处理好这个表项就能解决页面变更问题,这就将一颗树中多连接的问题本质上变成了一个单连接问题,然后使用链表的思路就可以解决。

四、实验数据

《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》
《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》

《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》
《读后感:The Bw-Tree: A B-tree for New Hardware Platforms》

费了半天劲,接下来该看结果了。由于本文着重写的是内存无锁之类的优化,所以比较前提也是数据全放入内存。可以看到比BerkeleyDB(传统B+-Tree)好几乎一个数量级,这是可以理解的,毕竟测试集不大,访问冲突率就会较高,而BerkeleyDB是page锁,冲突会比较严重一些。不过文中有一个细节,测试中BerkeleyDB设置的page size是8K,而[2]指出BerkeleyDB支持page size为512B到64K,那么如果降低page size,差距会不会缩小呢?

接下来跟skiplist比较,也好3-4+倍,这个作者给了一个合理的解释,就是CPU cache命中率不同是一个主要因素。skiplist是一个变种的链表,CPU cache命中率低是有共识的,这可能也意味着内存数据库使用skiplist为主要数据结构不太合适。

五、总结

虽然本文偏向内存数据库,文中也大多在说如何做到无锁和提高CPU cache命中率,但是其对mapping table的引入以及定位是非常有意思的地方,这种系统组合能让不同的子系统进行分别优化,从而有更大的机会充分利用硬件资源,不过遗憾的是,mapping table的结构、存储等细节讲的太少。

[1]. https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/bw-tree-icde2013-final.pdf

[2]. https://web.stanford.edu/class/cs276a/projects/docs/berkeleydb/ref/am_conf/pagesize.html

    原文作者:杨东东
    原文地址: https://zhuanlan.zhihu.com/p/37365403
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞