HBase的一个列簇(Column Family)本质上就是一棵LSM树(Log-StructuredMerge-Tree)。LSM树分为内存部分和磁盘部分。内存部分是一个维护有序数据集合的数据结构。通常来说,内存数据结构能够选择平衡二叉树、红黑树、跳跃表(SkipList)等维护有序集的数据结构,这里因为考虑并发性能,HBase选择了表现更优秀的跳跃表。磁盘部分是由一个个独立的文件组成,每个文件又是由一个个数据块组成。算法
LSM树本质上和B+树同样,是一种磁盘数据的索引结构。但和B+树不一样的是,LSM树的索引对写入请求更友好。由于不管是何种写入请求,LSM树都会将写入操做处理为一次顺序写,而HDFS擅长的正是顺序写(且HDFS不支持随机写),所以基于HDFS实现的HBase采用LSM树做为索引是一种很合适的选择。数组
LSM树的索引通常由两部分组成,一部分是内存部分,一部分是磁盘部分。内存部分通常采用跳跃表来维护一个有序的KeyValue集合。磁盘部分通常由多个内部KeyValue有序的文件组成。数据结构
通常来讲,LSM中存储的是多个KeyValue组成的集合,每个KeyValue通常都会用一个字节数组来表示。这里,首先须要来理解KeyValue这个字节数组的设计。并发
以HBase为例,这个字节数组串设计如图所示。
整体来讲,字节数组主要分为如下几个字段。其中Rowkey、Family、Qualifier、Timestamp、Type这5个字段组成KeyValue中的key部分。异步
• keyLen:占用4字节,用来存储KeyValue结构中Key所占用的字节长度。
• valueLen:占用4字节,用来存储KeyValue结构中Value所占用的字节长度。
• rowkeyLen:占用2字节,用来存储rowkey占用的字节长度。
• rowkeyBytes:占用rowkeyLen个字节,用来存储rowkey的二进制内容。
• familyLen:占用1字节,用来存储Family占用的字节长度。
• familyBytes:占用familyLen字节,用来存储Family的二进制内容。
• qualif ierBytes:占用qualif ierLen个字节,用来存储Qualif ier的二进制内性能
注意,HBase并无单独分配字节用来存储qualif ierLen,由于能够经过keyLen和其余字段的长度计算出qualif ierLen。代码以下:优化
• timestamp:占用8字节,表示timestamp对应的long值。
• type:占用1字节,表示这个KeyValue操做的类型,HBase内有Put、Delete、Delete Column、DeleteFamily,等等。注意,这是一个很是关键的字段,代表了LSM树内存储的不仅是数据,而是每一次操做记录。spa
Value部分直接存储这个KeyValue中Value的二进制内容。因此,字节数组串主要是Key部分的设计。设计
在比较这些KeyValue的大小顺序时,HBase按照以下方式(伪代码)来肯定大小关系:
注意,在HBase中,timestamp越大的KeyValue,排序越靠前。由于用户指望优先读取到那些版本号更新的数据。指针
上面以HBase为例,分析了HBase的KeyValue结构设计。一般来讲,在LSM树的KeyValue中的Key部分,有3个字段必不可少:
Key的二进制内容。
一个表示版本号的64位long值,在HBase中对应timestamp;这个版本号一般表示数据的写入前后顺序,版本号越大的数据,越优先被用户读取。甚至会设计必定的策略,将那些版本号较小的数据过时淘汰(HBase中有TTL策略)。
type,表示这个KeyValue是Put操做,仍是Delete操做,或者是其余写入操做。本质上,LSM树中存放的并不是数据自己,而是操做记录。这对应了LSM树(Log-Structured Merge-Tree)中Log的含义,即操做日志。
先看一个简单的问题:如今有K个文件,其中第i个文件内部存储有Ni个正整数(这些整数在文件内按照从小到大的顺序存储),如何设计一个算法将K个有序文件合并成一个大的有序文件?在排序算法中,有一类排序算法叫作归并排序,里面就有你们熟知的两路归并实现。如今至关于K路归并,所以能够拓展一下,思路相似。对每一个文件设计一个指针,取出K个指针中数值最小的一个,而后把最小的那个指针后移,接着继续找K个指针中数值最小的一个,继续后移指针……直到N个文件所有读完为止,如图所示。
算法复杂度分析起来也较为容易,首先用一个最小堆来维护K个指针,每次从堆中取最小值,开销为logK,最多从堆中取次元素。所以最坏复杂度就是
一个LSM树的索引主要由两部分构成:内存部分和磁盘部分。内存部分是一个ConcurrentSkipListMap,Key就是前面所说的Key部分,Value是一个字节数组。数据写入时,直接写入MemStore中。随着不断写入,一旦内存占用超过必定的阈值时,就把内存部分的数据导出,造成一个有序的数据文件,存储在磁盘上。LSM树索引结构如图所示。内存部分导出造成一个有序数据文件的过程称为flush。为了不f lush影响写入性能,会先把当前写入的MemStore设为Snapshot,再也不允许新的写入操做写入这个Snapshot的MemStore。另开一个内存空间做为MemStore,让后面的数据写入。一旦Snapshot的MemStore写入完毕,对应内存空间就能够释放。这样,就能够经过两个MemStore来实现稳定的写入性能。
LSM树索引结构
随着写入的增长,内存数据会不断地刷新到磁盘上。最终磁盘上的数据文件会愈来愈多。若是数据没有任何的读取操做,磁盘上产生不少的数据文件对写入并没有影响,并且这时写入速度是最快的,由于全部IO都是顺序IO。可是,一旦用户有读取请求,则须要将大量的磁盘文件进行多路归并,以后才能读取到所需的数据。由于须要将那些Key相同的数据全局综合起来,最终选择出合适的版本返回给用户,因此磁盘文件数量越多,在读取的时候随机读取的次数也会越多,从而影响读取操做的性能。
为了优化读取操做的性能,咱们能够设置必定策略将选中的多个hf ile进行多路归并,合并成一个文件。文件个数越少,则读取数据时须要seek操做的次数越少,读取性能则越好。
通常来讲,按照选中的文件个数,咱们将compact操做分红两种类型。一种是major compact,是将全部的hf ile一次性多路归并成一个文件。这种方式的好处是,合并以后只有一个文件,这样读取的性能确定是最高的;但它的问题是,合并全部的文件可能须要很长的时间并消耗大量的IO带宽,因此major compact不宜使用太频繁,适合周期性地跑。
另一种是minor compact,即选中少数几个hf ile,将它们多路归并成一个文件。这种方式的优势是,能够进行局部的compact,经过少许的IO减小文件个数,提高读取操做的性能,适合较高频率地跑;但它的缺点是,只合并了局部的数据,对于那些全局删除操做,没法在合并过程当中彻底删除。所以,minor compact虽然能减小文件,但却没法完全清除那些delete操做。而major compact能彻底清理那些delete操做,保证数据的最小化。
总结:LSM树的索引结构本质是将写入操做所有转化成磁盘的顺序写入,极大地提升了写入操做的性能。可是,这种设计对读取操做是很是不利的,由于须要在读取的过程当中,经过归并全部文件来读取所对应的KV,这是很是消耗IO资源的。所以,在HBase中设计了异步的compaction来下降文件个数,达到提升读取性能的目的。因为HDFS只支持文件的顺序写,不支持文件的随机写,并且HDFS擅长的场景是大文件存储而非小文件,因此上层HBase选择LSM树这种索引结构是最合适的。
文章基于《HBase原理与实践》一书