正文
本文继续论文读后感序列。读后感并不全文翻译,重点关心的是问题、思路、关键细节(当然对关键的理解因人而异)以及测试数据,欢迎交流。
一、问题
新的硬件的出现促使人们重新思考数据管理系统应该如何演进。文中描述的新的硬件趋势主要是固态硬盘以及多核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. 最后的测试也是全内存测试),虽然文中也提到系统中的某些策略对磁盘也适用,但是通篇来看,不是重点。
二、思路
顺着上面的问题,作者提出的解决办法有如下几条。
使用mapping table增加page数据管理的灵活性:如果我们把B+-Tree最下面的一层看做数据层,而上面的各层看做索引层,那么这种操作做到了将索引和数据使用两个子系统来分开管理,这样两个子系统可以分别优化,索引子系统优化的目标是高并发,高cache命中率,数据子系统优化的目标是充分利用随机读和顺序写的优势,减少随机写。mapping table是两个子系统的连接点,一个关键组件。
使用delta+base代替in-place更新:base可以理解为某节点的第一次出现,delta可以理解为对base相应的修改(insert/update/delete),类似数据库redo。在更新Bw-tree节点的时候,在节点前面放一个delta,然后delta指向base,而不是直接修改base,这样同时达到了两个好处。一是避免多线程修改同一个页面的时候锁base降低并发度,二是因为base并未被修改,也不会破坏CPU cache有效性(如果base在cache里面的话)
通过一系列的原子操作实现分裂和合并:传统观念上,B+-Tree的分裂和合并不加锁是不可思议的,比如分裂场景,要从父页面删除一条指向子页面的记录,然后添加两条新的记录指向新分裂的两个页面(或者修改记录,再添加新记录)。如果不加锁,比如刚刚删除一条记录,另外的线程就来访问,结果就错了。文中处理此类多步骤原子性的问题有一个统一的思路,一是增加临时节点,为访问待更新节点建立冗余路径,然后CAS断老路接新路;二是在整个操作没有完成之前,任何试图访问受该操作影响的页面的线程都要拿过接力棒继续完成整个操作,然后才能继续。第二步实质上是一个等待,但是内存数据库中,碰到自己动手比等待其他人完成后通知更快。
只写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,看起来好像是用户事务和系统事务是不同的实例。
三、关键细节
首先看看系统架构。系统分三层,上面是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就分裂合并之类的,可大可小,业务确定),减少了分裂合并的次数,这就进一步减少了索引的修改。第三个是分离了内存和磁盘结构,使得各自都可以有更优的实现。
再来看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,读性能又恢复了。周而复始,这个模式很多系统都用,是一个读写平衡的问题。