《Hbase原理与实践》读书笔记——2.基础数据结构与算法

2.1 总体介绍

HBase的一个列簇(Column Family)本质上就是一棵LSM树(Log-StructuredMerge-Tree)。LSM树分为内存部分和磁盘部分。
内存部分是一个维护有序数据集合的数据结构。一般来讲,内存数据结构可以选择平衡二叉树、红黑树、跳跃表(SkipList)等维护有序集的数据结构,这里由于考虑并发性能,HBase选择了表现更优秀的跳跃表。
磁盘部分是由一个个独立的文件组成,每一个文件又是由一个个数据块组成。对于数据存储在磁盘上的数据库系统来说,磁盘寻道以及数据读取都是非常耗时的操作(简称IO耗时)。因此,为了避免不必要的IO耗时,可以在磁盘中存储一些额外的二进制数据,这些数据用来判断对于给定的key是否有可能存储在这个数据块中,这个数据结构称为布隆过滤器(Bloom Filter)。


2.2 跳跃表

2.2.1 跳跃表简介

跳跃表(SkipList)是一种能高效实现插入、删除、查找的内存数据结构,这些操作的期望复杂度都是O(logN)。与红黑树以及其他的二分查找树相比,跳跃表的优势在于实现简单,而且在并发场景下加锁粒度更小,从而可以实现更高的并发性。正因为这些优点,跳跃表广泛使用于KV数据库中,诸如Redis、LevelDB、HBase都把跳跃表作为一种维护有序数据集合的基础数据结构。


2.2.2 跳跃表和链表

众所周知,在维护有序数据集合上,大家的第一反应可能是链表,在已经找到待操作的节点的情况下,链表的插入和删除的时间复杂度只有O(1),但是查询的时间复杂度却是O(n),需要逐个查找,而跳表正是为了优化链表的查询复杂度而被提出,它在链表之上额外存储了一些节点的索引信息达到避免依次查找元素的目的,从而将查询复杂度优化为O(logN)。


2.2.3 跳跃表定义

•跳跃表由多条分层的链表组成(设为S0, S1, S2, … , Sn),例如图中有6条链表。
•每条链表中的元素都是有序的。
•每条链表都有两个元素:+∞(正无穷大)和- ∞(负无穷大),分别表示链表的头部和尾部。
•从上到下,上层链表元素集合是下层链表元素集合的子集,即S1是S0的子集,S2是S1的子集。
•跳跃表的高度定义为水平链表的层数。



在这里插入图片描述


2.2.4 跳跃表查找流程

•以左上角元素(设为currentNode)作为起点
•如果发现currentNode后继节点的值小于等于待查询值,则沿着这条链表向后查询,否则,切换到当前节点的下一层链表。
•继续查询,直到找到待查询值为止(或者currentNode为空节点)为止。

图片表示查找元素5的流程



在这里插入图片描述


2.2.5 跳跃表插入流程

1、需要按照上述查找流程找到待插入元素的前驱和后继。
2、按照抛硬币算法生成一个高度值height(比如以1/2的概率决定高度是否+1,如果成功+1继续上述概率事件,如果失败,则高度确定下来)。
3、将待插入节点按照高度值height生成一个垂直节点(这个节点的层数正好等于高度值),之后插入到跳跃表的多条链表中去,这里有两种情况(即height > 跳跃表高度 和 height <= 跳跃表高度)
•如果heigh > 跳跃表的高度,那么跳跃表的高度被提升为height,同时需要更新头部节点和尾部节点的指针指向。
•如果height <= 跳跃表的高度,那么需要更新待插入元素前驱和后继的指针指向。

图片表示插入元素48的流程


在这里插入图片描述


2.2.6 跳跃表的性质

1、一个节点落在第k层的概率为p^(k-1)。
2、跳跃表的空间复杂度为O(n)。
3、跳跃表的高度为O(logn)。
4、跳跃表的查询时间复杂度为O(logN)。
5、跳跃表的插入/删除时间复杂度为O(logN)。


2.3 LSM树

LSM树本质上和B+树一样,是一种磁盘数据的索引结构。但和B+树不同的是,LSM树的索引对写入请求更友好。因为无论是何种写入请求,LSM树都会将写入操作处理为一次顺序写,而HDFS擅长的正是顺序写(且HDFS不支持随机写),因此基于HDFS实现的HBase采用LSM树作为索引是一种很合适的选择。

LSM树的索引一般由两部分组成,一部分是内存部分,一部分是磁盘部分。内存部分一般采用跳跃表来维护一个有序的KeyValue集合。磁盘部分一般由多个内部KeyValue有序的文件组成。


2.3.1.KeyValue存储格式

一般来说,LSM中存储的是多个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的二进制内容。
• qualifierBytes:占用qualifierLen个字节,用来存储Qualifier的二进制内容。注意,HBase并没有单独分配字节用来存储qualifierLen,因为可以通过keyLen和其他字段的长度计算出qualifierLen:qualifierLen = keyLen - 2B -rowkeyLen - 1B - familyLen -8B - 1B
• timestamp:占用8字节,表示timestamp对应的long值。
• type:占用1字节,表示这个KeyValue操作的类型,HBase内有Put、Delete、Delete Column、DeleteFamily,表明了LSM树内存储的不只是数据,而是每一次操作记录。

Value部分直接存储这个KeyValue中Value的二进制内容。所以,字节数组串主要是Key部分的设计。


2.3.2 LSM树的索引结构

一个LSM树的索引主要由两部分构成:内存部分和磁盘部分。内存部分是一个ConcurrentSkipListMap,Key就是前面所说的Key部分,Value是一个字节数组。数据写入时,直接写入MemStore中。随着不断写入,一旦内存占用超过一定的阈值时,就把内存部分的数据导出,形成一个有序的数据文件,存储在磁盘上。

LSM树索引结构如图。内存部分导出形成一个有序数据文件的过程称为flush。为了避免flush影响写入性能,会先把当前写入的MemStore设为Snapshot,不再容许新的写入操作写入这个Snapshot的MemStore。另开一个内存空间作为MemStore,让后面的数据写入。一旦Snapshot的MemStore写入完毕,对应内存空间就可以释放。这样,就可以通过两个MemStore来实现稳定的写入性能。


在这里插入图片描述

随着写入的增加,内存数据会不断地刷新到磁盘上。最终磁盘上的数据文件会越来越多。如果数据没有任何的读取操作,磁盘上产生很多的数据文件对写入并无影响,而且这时写入速度是最快的,因为所有IO都是顺序IO。但是,一旦用户有读取请求,则需要将大量的磁盘文件进行多路归并,之后才能读取到所需的数据。因为需要将那些Key相同的数据全局综合起来,最终选择出合适的版本返回给用户,所以磁盘文件数量越多,在读取的时候随机读取的次数也会越多,从而影响读取操作的性能。

优化读取操作的性能,可以设置一定策略将选中的多个hfile进行多路归并,合并成一个文件。文件个数越少,则读取数据时需要seek操作的次数越少,读取性能则越好。
1、major compact:将所有的hfile一次性多路归并成一个文件
优势:合并之后只有一个文件,这样读取的性能肯定是最高的。
劣势:合并所有的文件可能需要很长的时间并消耗大量的IO带宽,因此,major compact不宜使用太频繁,适合周期性地跑。

2、minor compact:选中少数几个hfile,将它们多路归并成一个文件
优势:进行局部的compact,通过少量的IO减少文件个数,提升读取操作的性能,适合较高频率地跑。
劣势:只合并了局部的数据,对于那些全局删除操作,无法在合并过程中完全删除。因此,minor compact虽然能减少文件,但却无法彻底清除那些delete操作。而major compact能完全清理那些delete操作,保证数据的最小化。


2.3.3 总结

LSM树的索引结构本质是将写入操作全部转化成磁盘的顺序写入,极大地提高了写入操作的性能。但是,这种设计对读取操作是非常不利的,因为需要在读取的过程中,通过归并所有文件来读取所对应的KV,这是非常消耗IO资源的。因此,在HBase中设计了异步的compaction来降低文件个数,达到提高读取性能的目的。由于HDFS只支持文件的顺序写,不支持文件的随机写,而且HDFS擅长的场景是大文件存储而非小文件,所以上层HBase选择LSM树这种索引结构是最合适的。


2.4 布隆过滤器

2.4.1 布隆过滤器案例说明

如何高效判断元素w是否存在于集合A之中?
1、哈希表 (解决小数据量场景下元素存在性判定,但如果A中元素数量巨大,甚至数据量远远超过机器内存空间,则无能为力)
2、基于磁盘和内存的哈希索引 (覆盖大数据量场景但实现成本不低)
3、布隆过滤器 (覆盖大数据量场景,且低成本)

布隆过滤器由一个长度为N的01数组array组成。首先将数组array每个元素初始设为0。对集合A中的每个元素w,做K次哈希,第i次哈希值对N取模得到一个index(i),即index(i)=HASH_i(w)%N,将array数组中的array[index(i)]置为1。最终array变成一个某些元素为1的01数组。
下面举个例子,如图所示,A={x, y, z},N=18,K=3。


在这里插入图片描述

x,y,z三个元素迭代hash取模后,最终得到的布隆过滤器串为:010111000001010010。
但对于元素w,迭代hash取模后的结果下标分别是4、13、15,其中布隆过滤器的第15位为0,因此可以确认w肯定不在集合A中。

布隆过滤器串对任意给定元素w,给出的存在性结果为两种:
•w可能存在于集合A中。(当hash迭代取模的下标的元素全为1)
•w肯定不在集合A中。 (当hash迭代取模的下标的元素存在为0)
这说明布隆过滤器存在误判率(所谓误判率也就是过滤器判定元素可能在集合中但实际不在集合中的占比),他只能证明某个元素一定不在集合中,但不能肯定某个元素在集合中。

有论文中证明,当N取K*|A|/ln2时(其中|A|表示集合A元素个数),能保证最佳的误判率。举例来说,若集合有20个元素,K取3时,则设计一个N=3×20/ln2=87二进制串来保存布隆过滤器比较合适。


2.4.2 布隆过滤器与Hbase

由于布隆过滤器只需占用极小的空间,便可给出“可能存在”和“肯定不存在”的存在性判断。HBase的Get操作就是通过运用低成本高效率的布隆过滤器来过滤大量无效数据块的,从而节省大量磁盘IO。

在HBase 1.x版本中,用户可以对某些列设置不同类型的布隆过滤器,共有3种类型。
NONE:关闭布隆过滤器功能。
ROW:按照rowkey来计算布隆过滤器的二进制串并存储。Get查询的时候,必须带rowkey,所以用户可以在建表时默认把布隆过滤器设置为ROW类型。
ROWCOL:按照rowkey+family+qualifier这3个字段拼出byte[]来计算布隆过滤器值并存储。如果在查询的时候,Get能指定rowkey、family、qualifier这3个字段,则可以通过布隆过滤器提升性能

注意:一般意义上的Scan操作,HBase都没法使用布隆过滤器来提升扫描数据性能,但对于ROWCOL类型的布隆过滤器来说,如果在Scan操作中明确指定需要扫某些列,则同样可以借助布隆过滤器提升性能,如下所示:


在这里插入图片描述

Scan过程中,碰到KV数据从一行换到新的一行时,是没法走ROWCOL类型布隆过滤器的,因为新一行的key值不确定;但是,如果在同一行数据内切换列时,则能通过ROWCOL类型布隆过滤器进行优化,因为rowkey确定,同时column也已知,也就是说,布隆过滤器中的Key确定,所以可以通过ROWCOL优化性能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值