HBase深度历险 | 京东物流技术团队
简介
HBase 的全称是 Hadoop Database,是一个分布式的,可扩展,面向列簇的数据库,是一个通过大量廉价的机器解决海量数据的高速存储和读取的分布式数据库解决方案。本文会像剥洋葱一样,层层剥开她的心。
特点
首先我们看一下hbase有哪些特点:
基于LSM树的数据结构设计,保证了顺序写,并且通过布隆过滤器,compaction等内部优化手段来优化读性能,使得hbase具有很高的读写性能。
hbase在数据写入之前,会先写入WAL预写日志,用来防止机器宕机时,内存中数据的丢失问题。
底层依赖hdfs,当磁盘空间不足时,可以直接水平扩展。
稀疏性是 HBase 中的一个突出的特点,在其他数据库中,对于空值的处理一般都会填充 null,对于成百上千万列的表来说,通常会存在大量的空值,如果使用填充 null 的策略,势必会造成大量空间的浪费。而对于 HBase 空值不需要任何填充,因此稀疏性是 HBase 的列可以无限扩展的一个重要的条件。
使用了列簇存储,可以使用户自由选择哪些列来放在同一个列簇中
hbase支持数据多版本的保存,通过时间戳来排序。用户可以根据需要选择最新版本或者某个历史版本。
什么是列簇存储
前边提到了hbase是按照列簇存储来存储数据的,我们先来对比一下行式存储,列式存储以及列簇存储的区别。
传统的关系型数据库都是按行存储的,也就是每行数据都是存储在一起的。
数据按列存储,就是每列数据是存储在一起的。按列存储有好处呢?
那什么是列簇存储呢?按列存储是每个列单独存储在一起,这种方式对于字段很多的表来说,如果一次查询设计的列数量太多的话,势必会造成大量的磁盘IO,从而影响查询性能。而列簇存储的意思就是,一个列簇下边可以存储多个列,每个列簇存储在一个文件里,这样就能减少一些磁盘IO,从而提升查询性能。
每个列簇的数据存储在一起,每个列簇可以自由选择储存哪些列。所以列簇存储实际上是给用户提供了一种自由选择的权利,如果所有的列都放到一个列簇里,那实际就相当于按行存储一样,每次查询需要把所有的列全部都查出来,而如果每个列单独存一个列簇的话,就像按列存储一样了。
在当前体系中不建议设置太多列簇,后面也会提到,是因为MemStore在进行flush时候的最小单元不是MemStore,而是整个region,因此设置太多的列簇会是flush十分耗能,但是这种架构为 HBase 将来演变成 HTAP(Hybrid Transactional and Analytical Processing)系统提供了最核心的基础。
架构原理
HBase架构整体上分为3个部分Zookeepper集群,HMaster以及Region server。架构图看上去还挺复杂的,但是最核心的是region server,后边会从region server -> region -> store -> hfile-> data block,一点点的拨开它的外衣。
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-0130X9AbBHXXIx78EZ.png)
1.Zookeeper
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01wwZkh5icy5YJqHZ.png)
Zookeepper是一个分布式的无中心的元数据存储服务,用来探测和记录HBase集群中服务器的状态信息,hmaster和region server通过定时发送心跳和zk维持关系。
Zookeepper中存储了hbase:meta 表,这张表维护了整个集群所有的 Region 信息,client首先会访问zk,查询hbase:meta 表信息,为了查询性能会将hbase:meta 表信息缓存在客户端。
2.HMaster
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01CXWIUl30116gNQdnH.png)
HMaster在hbase中是属于一个leader的角色,不参与具体表数据的管理,只负责宏观把控,主要工作内容有两方面,一个是管理Region server,另一个就执行一些高危操作,比如执行DDL(建表,删除表等)。
HBase Master的功能:
3.Region server
Region server是整个habse架构最为核心的模块,负责实际数据的读写,当访问数据时, 客户端与HBase的Region server直接通信。
HBase的表根据Row Key的区域分成多个Region, 一个Region包含这这个区域内所有数据。而Region server负责管理多个Region, 负责在这个Region server上的所有region的读写操作,一个Region server最多可以管理1000个region。
每个 Region server都把自己的数据存储在HDFS中,如果一个服务器既是Region server又是HDFS的Datanode. 那么这个Region server的数据会在把其中一个副本存储在本地的HDFS中, 加速访问速度。
但是, 如果是一个新迁移来的Region server, 这个region server的数据并没有本地副本. 直到HBase运行compaction, 才会把一个副本迁移到本地的Datanode上面。
3.1 Region server架构图
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01Qx9jIehLi6gdqKV.png)
RegionServer 主要用来响应用户的 IO 请求,是 HBase 中最核心的模块,由 WAL(HLog)、BlockCache 以及多个 Region 构成。
3.2 HLog(WAL)
HLog其实就是WAL (Write-Ahead-Log) 预写日志,就是在写入memstore之前,会先写到HLog,给数据来个备份,HLog是保存在磁盘上的,从而保证高可靠性。HLog是region server级别的,也就是说整个region server共用一个HLog。
HLog在hbase中有两个作用:
HLog 的日志文件存放在 HDFS 中,hbase 集群默认会在 hdfs 上创建 hbase 文件夹,在该文件夹下有一个 WAL 目录,其中存放着所有相关的 HLog,HLog 并不会永久存在,在整个 HBase 总 HLog 会经历如下过程:
3.3 BlockCache
HBase会将从HFile查询出的数据缓存到BlockCache,以便后续可以直接从内存读取,减少磁盘IO。
BlockCache是region级别的,每个region只有一个BlockCache。
HBase的数据仅仅独立存在于MemStore和HFile中,BlockCache中缓存的只是HFile中的部分热点数据。
3.4 region
每个region由一个或多个store组成,region是集群负载均衡的基本单位,是MemStore进行flush的基本单元。整个region其实就是一个LSM树,memstore对应C0树,存储在内存中,作为写缓存,HFile对应Cn树,存储在磁盘上。通过保证顺序写,在牺牲部分读性能的前提下,来大幅度提升写入性能,并且通过布隆过滤器和compaction等内部的优化来弥补读性能,从而达到读写性能都很高。
3.4.1 store
每个store由MemStore和多个StoreFile组成,StoreFile底层实际是HFile,StoreFile是hbase对HFile的封装。每个列簇存储在一个store,所以有几个列簇,就会有几个store,列簇太多就会有过多的MemStore,就会占用过多的内存,并且在进行flush的时候,也会造成更大的耗能。
3.4.1.1 MemStore
MemStore是hbase的写缓存,在写入请求时,当写入HLog成功之后,变先写入MemStore,并且会按照rowkey字典序进行排序,当达到一定阈值的时候,才会flush数据到hdfs形成hfile文件。hbase的MemStore是采用了跳表这一数据结构,在这里就不过多介绍了。
MemStore的作用:
MemStore在读写时都起到了很大的作用,最大的耗能操作时在flush操作,下边我们详细介绍一下。
Memstore Flush触发条件
我们已经知道,当MemStore的大小达到一定的阈值时,就会flush数据到HDFS上,形成hfile文件,但是需要注意的是,MemStore进行flush的最小操作单元不是MemStore,而是整个HRegion。也就是说,有一个MemStore需要进行flush,整个HRegion都会受影响,所以一个HRegion如果有过多的Memstore,每次flush的开销必然会很大,所以每个表不应该设置过多的列簇。下边介绍一下触发flush操作的具体条件:
hbase.hregion.memstore.flush.size
(默认值128MB)时,会触发该Region中所有MemStore Flush,不会阻塞写操作。hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size
(默认值2 * 128 = 256MB)时,会触发该Region中所有MemStore Flush,期间阻塞该Region的写操作。hbase.hregion.preclose.flush.size
(默认值5MB)时,会触发该Region中所有MemStore Flush,然后Region才能关闭。hbase.regionserver.global.memstore.size * HBASE_HEAPSIZE
(默认值0.4 * 堆空间大小)时,会从MemStore最大的Region开始,触发该RegionServer中所有Region的Flush,并阻塞整个RegionServer的写操作。直到MemStore大小回落到上一个参数值的hbase.regionserver.global.memstore.size.lower.limit
(默认值0.95)倍,才解除阻塞。hbase.regionserver.maxlogs
(默认值32)时,HBase就选取最早的一个WAL对应的那些Region进行MemStore Flush,期间也会阻塞对应Region的写操作。hbase.regionserver.optionalcacheflushinterval
(默认值1小时)。为了避免所有Region同时Flush,定期刷新会有随机的延时。flush [table]
或flush [region]
命令来手动Flush一张表或一个Region的MemStore。
3.4.1.2 HFile
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01cVoJKCpmYwfHtcu.png)
HFile是HBase存储数据的文件组织形式,参考BigTable的SSTable和Hadoop的TFile实现。下图是HFile扥物理结构示意图,如图所示,HFile会被切分为多个大小相等的block块,HFile内部结构还是比较复杂的,有兴趣的同学可以看下(http://hbasefly.com/2016/03/25/hbase-hfile/),本文我们主要看下存储实际数据的data block。
3.4.1.2.1 DataBlock
DataBlock是HBase中数据存储的最小单元。DataBlock中主要存储用户的KeyValue数据(KeyValue后面一般会跟一个timestamp,图中未标出),而KeyValue结构是HBase存储的核心,每个数据都是以KeyValue结构在HBase中进行存储。KeyValue结构磁盘中可以表示为:
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01RWieHN30o22E7GUre.png)
每个KeyValue都由4个部分构成,分别为key length,value length,key和value。其中key length和value length是两个固定长度的数值,而key是一个复杂的结构,首先是rowkey的长度,接着是rowkey,然后是ColumnFamily的长度,再是ColumnFamily,之后是ColumnQualifier,最后是时间戳和KeyType(keytype有四种类型,分别是Put、Delete、 DeleteColumn和DeleteFamily),value就没有那么复杂,就是一串纯粹的二进制数据。
3.4.1.2.2 基本概念
从上图可以看出一些长度是固定的值,所以把key做一些简化,然后重点看下这些内容的具体含义。Key由RowKey(行键) + ColumnFamily(列族)+ Column Qualifier(列修饰符)+ TimeStamp(时间戳--版本)+ KeyType(类型)组成,而Value就是实际上的值。
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01PaxfjdIv30c8uqzq.png)
4.读写流程
在介绍读写流程之前,先介绍一下Meta table,为什么要先介绍它,因为它是读写操作的必经之路,是rowkey的引路人。
Meta table
Meta table存储了所有的region信息,Meta table储存在zk中,客户端在第一次读写访问或者region失效时,都会先访问zk,根据rowkey从Meta table获取region server的信息,然后客户端再去访问对应的region server,进行对应的读写操作。
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01NX6hN9m6TbbfIkb.png)
写流程
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01sR9ej58cAcddQaM.png)
读流程
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01slpYAtx9ysRfoYn.png)
读流程详解
hbase的写操作十分方便,更新实际上只是新加了一条最新时间戳的数据,删除数据也只是添加了一个delete的标记,只有再Major Compaction的时候才会进行物理删除。而因为hbase中同一个rowkey是保存了多版本的数据,以及不同的keytype的数据,所以同一个rowkey会对应多条数据,因此这里不像我们想的那样,如果rowkey命中BlockCache或者MemStore就会直接返回,远没有想象中的简单,因此也不存在先从BlockCache读还是先从MemStore中读的概念,这种说法本身就是不对的。
在读取数据的时候,内存MemStore和磁盘HFile中的数据都要读取,会创建两种类型的Scanner(StoreFileScanner和MemstoreScanner),分别用来探查HFile和MemStore中的数据,然后会根据用户选择的时间范围以及rowkey的范围过滤掉一些Scanner,最后对于HFile的数据会先查找对应的Block,查找Block时会优先从Blockcache中查找,找不到再从HFile中加载。而MemstoreScanner会从Memstore中查询数据,最后将内存和磁盘中的数据进行合并,返回最新的数据给到客户端。简化的流程如下:
每个StoreScanner会为当前该Store中每个HFile构造一个StoreFileScanner,用于实际执行对应文件的检索。同时会为对应Memstore构造一个MemstoreScanner,用于执行该Store中Memstore的数据检索。
根据Time Range以及RowKey Range对StoreFileScanner以及MemstoreScanner进行过滤,淘汰肯定不存在待检索结果的Scanner。
所有StoreFileScanner开始做准备工作,在负责的HFile中定位到满足条件的起始Row。Seek过程(此处略过Lazy Seek优化)也是一个很核心的步骤,它主要包含下面三步:
6.HBase Compaction
HBase 的 MemStore 在达到触发条件的时候,会把MemStore中的数据flush到HDFS上,每次都会形成一个新的HFile文件,所以随着时间的不断累积,同一个 Store 下的 HFile 会越来越多,进而会降低 HBase 查询性能,主要体现在查询数据的 IO 次数增加。为了优化查询性能,HBase 会合并小的 HFile 以减少文件数量,这种合并 HFile 的操作称为 Compaction。
![](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-11-22-17-01kNXkEs22iVlZtT22O.png)
HBase与传统关系型数据库的对比
没有数据类型,都是字节数组(有一个工具类Bytes,将java对象序列化为字节数组),而传统的关系型数据库有丰富的数据类型。
HBase只有很基本的插入、查询、删除等操作,表和表之间是没有关系的,而传统数据库一般都会有很多的函数,并且表之间有关联的特点。
Hbase基于列簇存储,而传统关系型数据库是基于行存储。
habse中对于数据update情况,不会向传统数据库一样,直接修改这条记录,而是新插入了一条状态为delete的数据,这么设计的原因是为了保证顺序写,提高数据IO效率,进而提高性能。并且旧数据也不会马上删除,会在compaction时才会进行删除。
Hbase数据写入cell时,还会附带时间戳,默认为数据写入时RegionServer的时间,但是也可以指定一个不同的时间。数据可以有多个版本,并且可以设置TTL,以及保留的版本个数。
搞过mysql分库分表的同学一定体会过它的魅力,十分的麻烦,但是hbase可以动态水平扩展,十分便捷
Rowkey的设计
访问方式
HBase访问只有3种方式
RowKey设计原则
RowKey是一个二进制码流,可以是任意字符串,最大长度为64kb,实际应用中一般为10-100byte,以byte[]形式保存,一般设计成定长。建议越短越好,不要超过16个字节
必须在设计上保证RowKey的唯一性。由于在HBase中数据存储是Key-Value形式,若向HBase中同一张表插入相同RowKey的数据,则原先存在的数据会被新的数据覆盖。
HBase的RowKey是按照ASCII有序排序的
RowKey应均匀的分布在各个HBase节点上,防止热点数据造成数据倾斜。
避免数据热点的方法
顾名思义,直接反转的意思。比如用户id,手机号等,rowkey的头部不具有随机性,但是尾部具有良好的随机性,那这时我们可以将rowkey直接翻转过来。Reversing可以有效的使RowKey随机分布,但是牺牲了RowKey的有序性。利于Get操作,但不利于Scan操作,因为数据在原RowKey上的自然顺序已经被打乱。
Salting(加盐)的原理是在原RowKey的前面添加固定长度的随机数,从而保障数据在所有Regions间的负载均衡。
Hashing和加盐是类似的,只是Hashing要求前缀不能是随机的,需要使用一些hash算法,这样客户端可以重构出Hashing后的rowkey。
推荐方案:
一般情况,可以选择业务主键的后3位取余hbase分区数的余数作为加盐的前缀,然后在拼接上业务主键作为rowkey。
HBase的缺点
注意点总结
Q&A
1. 常说HBase数据读取要读Memstore、HFile和Blockcache,为什么上面Scanner只有StoreFileScanner和MemstoreScanner两种?没有BlockcacheScanner?
HBase中数据仅仅独立地存在于Memstore和StoreFile中,Blockcache中的数据只是StoreFile中的部分数据(热点数据),即所有存在于Blockcache的数据必然存在于StoreFile中。因此MemstoreScanner和StoreFileScanner就可以覆盖到所有数据。实际读取时StoreFileScanner通过索引定位到待查找key所在的block之后,首先检查该block是否存在于Blockcache中,如果存在直接取出,如果不存在再到对应的StoreFile中读取。
2. 数据更新操作先将数据写入Memstore,再落盘。落盘之后需不需要更新Blockcache中对应的kv?如果不更新,会不会读到脏数据?
如果理清楚了第一个问题,相信很容易得出这个答案:不需要更新Blockcache中对应的kv,而且不会读到脏数据。数据写入Memstore落盘会形成新的文件,和Blockcache里面的数据是相互独立的,以多版本的方式存在。
参考资料:
https://my.oschina.net/u/4511602/blog/4916109
https://www.jianshu.com/p/f911cb9e42de
https://www.jianshu.com/p/0e178bce8f63
http://hbasefly.com/2016/12/21/hbase-getorscan/
http://hbasefly.com/2017/06/11/hbase-scan-2/
https://zhuanlan.zhihu.com/p/145551967
https://blog.csdn.net/u012151684/article/details/109040581
https://xie.infoq.cn/article/76f8caba743f8be2beb81441d
https://zhuanlan.zhihu.com/p/159052841?utm_id=0
http://hbasefly.com/2016/04/03/hbase_hfile_index/
http://hbasefly.com/2016/03/25/hbase-hfile/
作者:京东物流 于建飞
来源:京东云开发者社区 自猿其说 Tech 转载请注明来源