HBase数据结构之 跳跃表、LSM、布隆过滤器

目录

  • LSM树
    • 用多路归并实现LSM树的文件合并
  • 用跳表实现LSM的内存部分
  • 布隆过滤器

LSM树

HBase中的LSM树

用多路归并实现LSM树的文件合并

  随着写入的增加,内存数据会不断地刷新到磁盘上。最终磁盘上的数据文件会越来越多。如果用户有读取请求,则需要将大量的磁盘文件进行多路归并,之后才能读取到所需的数据。
  LSM树的索引实际上是将写入操作全部转化为了磁盘的顺序写入,提高了写入性能。但是,这种设计是以牺牲一定的读操作性能为代价的,因为读取的时候,需要归并多个文件来获取满足条件的KV,非常消耗磁盘IO。所以HBase会通过compaction来合并小文件,降低文件个数,来提高读取效率。

用跳表实现LSM的内存部分

  HBase中对LSM树的实现,是在内存中用一个ConcurrentSkipListMap保存数据。数据写入时,直接写入MemStore中。随着不断写入,一旦内存占用超过一定的阈值时,就把内存部分的数据导出,形成一个有序的数据文件,存储在磁盘上。内存中的实现,采用了跳跃表(SkipList)的数据结构。
HBase数据结构之 跳跃表、LSM、布隆过滤器_第1张图片
跳跃表是一种能高效实现插入、删除、查找的内存数据结构,这些操作的期望复杂度都是O(logN)。跳跃表的优势在于比红黑树或其他的二分查找树的实现简单很多;并发场景下加锁粒度更小,提高并发性能。
  跳跃表可以看作是一种特殊的有序链表。跳跃表是由多层有序链表组成。最底一层的链表保存了所有的数据,为了提高链表的查询效率,通过每向上的一层链表依次保存下一层链表的部分数据作为索引,采用空间换取时间等方式提高效率。相邻的两层链表中元素相同的节点之间存在引用关系,一般是上层节点中存在一个指向下层节点的引用。
  跳跃表的目的在于提高了查询效率,同时也牺牲了一定的存储空间
在跳跃表中查找一个指定元素的流程比较简单。假如以左上角元素作为起点:如果发现当前节点的后继节点的值小于等于待查询值,则沿着这条链表向后查询,否则,切换到当前节点的下一层链表。继续查询,直到找到待查询值为止(或者当前节点为空节点)为止。
  跳跃表的构建稍微复杂一点。首先,需要按照上述查找流程找到待插入元素的前驱和后继;然后,按照如下随机算法生成一个高度值:

// p是一个(0,1)之间的常数,一般取p=1/4或者1/2
public void randomHeight(doubule p) {
    int height = 0;
    while(random.newtDouble < p) {
        height++;
    }
    return height + 1;
}

最后,将待插入节点按照randomHeight生成一个垂直节点的位置(这个节点的层数位置正好等于高度值),之后插入到跳跃表的多条链表中去。假设height=randomHeight§,这里需要分两种情况讨论:

  • 如果height大于跳跃表的高度,那么跳跃表的高度被提升为height,同时需要更新头部节点和尾部节点的指针指向
  • 如果height小于等于跳跃表的高度,那么需要更新待插入元素前驱和后继的指针指向

布隆过滤器

  除了使用compaction归并小文件外,HBase还利用布隆过滤器来提高读取性能。布隆过滤器是一个 bit 向量或者说 bit 数组,数组由一个长度为N的0、1组成。布隆过滤器串对任意给定元素w,给出的存在性结果为两种:

  • w可能存在于集合A中
  • w肯定不在集合A中

  在知道HBase为什么要引入布隆过滤器之前,先了解HBase存储文件HFile的块索引机制。HBase的实际存储结构是HFile,它是位于HDFS系统中的,也就是在磁盘中。而加载到内存中的数据存储在MemStore中,当MemStore中的数据达到一定数量时,它会将数据存入HFile中。HFIle是由一个个数据块与索引块组成,通常默认为64KB。HBase是通过块索引来访问这些数据块的。而索引是由每个数据块的第一行数据的rowkey组成的。当HBase打开一个HFile时,块索引信息会优先加载到内存当中。然后HBase会通过这些块索引来查询数据。
  当随机读get数据时,如果采用HBase的块索引机制,HBase会加载很多块文件。如果采用布隆过滤器后,它能够准确判断该HFile的所有数据块中,是否含有查询的数据,从而大大减少不必要的块加载,从而增加HBase集群的吞吐率。有了布隆过滤器这样一个存在性判断之后,把集合A的元素按照顺序分成若干个块,每块不超过64KB,每块内的多个元素都算出一个布隆过滤器串,多个块的布隆过滤器组成索引数据。为了判断元素w是否存在于集合A中,先对w计算每一个块的布隆过滤器串的存在性结果,若结果为肯定不存在,则继续判断w是否可能存在于下一个数据块中。若结果为可能存在,则读取对应的数据块,判断w是否在数据块中,若存在则表示w存在于集合A中;若不存在则继续判断w是否在下一个数据块中。
  正是由于布隆过滤器只需占用极小的空间,便可给出“可能存在”和“肯定不存在”的存在性判断,因此可以提前过滤掉很多不必要的数据块,从而节省了大量的磁盘IO。HBase的Get操作就是通过运用低成本高效率的布隆过滤器来过滤大量无效数据块的,从而节省大量磁盘IO。

1、布隆过滤器的存储在哪?
  对于HBase而言,当选择采用布隆过滤器之后,HBase会在生成StoreFile(HFile)时包含一份布隆过滤器结构的数据,称其为MetaBlock;MetaBlock与DataBlock(真实的KeyValue数据)一起由LRUBlockCache维护。所以,开启bloomfilter会有一定的存储及内存cache开销。

2、采用布隆过滤器后,HBase如何get数据?
  在读取数据时,HBase会首先在布隆过滤器中查询,根据布隆过滤器的结果,再在MemStore中查询,最后再在对应的HFile中查询。

3、采用ROW还是ROWCOL布隆过滤器?
  这取决于用户的使用模式。如果用户只做行扫描,使用更加细粒度的行加列布隆过滤器不会有任何的帮助,这种场景就应该使用行级布隆过滤器。当用户不能批量更新特定的一行,并且最后的使用存储文件都含有该行的一部分时,行加列级的布隆过滤器更加有用。

补充:Bloom Filter
场景:快速从海量数据中找出某个成员是否属于这个集合:

特长:
  因为Bloom Filter使用位数组和哈希函数来表征集合,并不需要存储集合数据本身内容,所以其空间利用率非常高。

基本思想:
  长度为 m 的位数组来存储集合信息,k 个相互独立的哈希函数将数据映射到数组空间。对于集合S中的某个成员a,分别使用k个哈希函数对其进行计算,如果 H i(a)=x(1<=i<=k,1<=x<=m),则将位数组的第x 位置为1,

查询:
  当查询某个成员a是否在集合S中出现时,使用相同的k个哈希函数计算,如果其对应位置全部为1,则a属于集合S,只要有一个位置为0,则a 不属于集合S。

误判率及相关计算:
为什么会发生误判: 假如此时查询X3这个元素是否属于集合,通过3个哈希函数计算后的位置数为 2,7,11,而这时这3个位置都为1,BF会认为X3属于集合,但可能此时这3个位置的1是其他元素对应的,就发生误判了。

影响误判率的因素:

  1. 集合大小n
    因为集合n越大,其它条件固定的情况下,位数组中就会有更多比例的位置被设成1,误判率就会增大。
  2. 哈希函数的个数k
    个数越多,位数组中更多比例的位置被设置为1,即增大了误判率。但在查询时,显然个数越多的时候误判率会越低。
  3. 位数组的大小m
    位数组大小m越大,那么n和k固定的情况下,位数组中剩余0的比特位就越高,误判率就会减小。

你可能感兴趣的:(HBASE)