由来
Hadoop Database
2006年末发起,根据Google的Chang等人发表的论文“Bigtable:A Distributed Storage System for Strctured Data“来设计的。
依赖
1.Hadoop HDFS作为存储系统;
2.Hadoop MapReduce作为计算系统;
3.Zookeeper作为协调工具。Hbase通过Zookeeper来做master的高可用、RegionServer的监控、元数据的入口以及集群配置的维护等工作。
数据模型
逻辑模型
- 表(table):只能存字符串,以字节码形式存储
- 行(row):由行键(rowkey)唯一标识
- 列族(column failmy):行的数据按列族分组,创建表时定义
- 列限定符(column qualifier):列的最小粒度,定位列数据
- 时间版本(version):单元的值区分不同时间版本,用时间戳(timestamp)来标识
- 单元(cell):行健、列族、列限定符、时间版本一起确定一个单元
逻辑模型详解
(1)行键(rowkey):表中行根据行键进行排序,数据按照rowkey的字节序列,即字典顺序存储;所有对表的访问都要通过行键,对HBase的查询只包含三种:单个rowkey访问;rowkey范围访问;全表扫描。Rowkey对Hbase的性能影响非常大,Rowkey的设计就显得尤为的重要。设计的时候要兼顾基于Rowkey的单行查询也要键入Rowkey的范围扫描。
(2)列族(column family):HBase所谓的列式存储就是根据column failmy
(3)列限定符(column qualifier):列限定符定位单元,列限定符可以在存储时动态添加
(4)时间戳(timestamp):每个单元可能又有多个版本,它们之间用时间戳区分。相同rowkey的数据按照timestamp倒序排列。默认查询的是最新的版本,用户可同指定timestamp的值来读取旧版本的数据。
(5)单元(cell):由行键、列族、限定符、时间戳唯一决定。cell中的数据是没有类型的,全部以字节码形式存贮
(6)对行的写操作是始终是“原子”的
物理模型
(1)Client:Client包含了访问Hbase的接口,另外Client还维护了对应的cache来加速Hbase的访问,比如cache的.META.元数据的信息。
(2)HMaster:主要负责
- HTable(表)和HRegion(按照rowkey将表横向切分)的管理工作;
- 为HRegionServer分配HRegion;
- 维护集群的元数据信息;
- 维护整个集群的负载均衡;
- 发现失效的HRegion,并将失效的HRegion分配到正常的HRegionServer上。
(3)HRegionServer:主要负责
- 响应用户I/O请求;
- 向HDFS文件系统中读写数据;
- 负责Region变大以后的拆分;
- 负责Storefile的合并工作。
(4)HRegion:负责了Table中的一个HRegion,它包含了一个HLog部分和多个HStore。HLog部分保存着用户操作hbase的日志,用户的操作都会先记录到HLog中,然后再保存到HRegion中。
(5)Store:对应了Table中的一个Column Family的存储。它由一个MemStore和多个StoreFile组成。可以看出每个Column Family其实就是一个集中的存储单元,因此最好将具备共同I/O特性的column放在一个Column Family中,这样最高效。
(6)HLog:当数据保存到MemStore,最后却没有保存到HFile中时,死机了。用户操作的指令保存在HLog中,它会将指令执行,再将重新保存到MemStore中,这样就可以完成后面的操作了。
(7)MemStore:MemStore驻留内存。当数据保存时,数据会先存储到MemStore中,然后根据用户设定的显式刷写或隐式刷写(默认)模式,将数据再保存到StoreFile中。
(8)StoreFile:底层实现是HFile,当StoreFile文件数量增长到一定阈值,会触发Compact合并操作,将多个StoreFile合并成一个StoreFile。
(9)HFile:存储列族数据,HBase中的最小单位了。但也可以进行拆分,也就是所谓的分区,让数据更加分散,读取数据的时候更加效率。
流程
老的Region寻址方式
在Hbase 0.96版本以前,Hbase有两个特殊的表,分别是-ROOT-表和.META.表,其中-ROOT-的位置存储在ZooKeeper中,-ROOT-本身存储了.META.Table的RegionInfo信息,并且-ROOT-不会分裂,只有一个region。而.META.表可以被切分成多个region。结构图和读取的流程如下图所示:
从上面的路径我们可以看出,用户需要3次请求才能直到用户Table真正的位置,这在一定程序带来了性能的下降。在0.96之前使用3层设计的主要原因是考虑到元数据可能需要很大。但是真正集群运行,元数据的大小其实很容易计算出来。在BigTable的论文中,每行METADATA数据存储大小为1KB左右,如果按照一个Region为128M的计算,3层设计可以支持的Region个数为2^34个,采用2层设计可以支持2^17(131072)。那么2层设计的情况下一个集群可以存储4P的数据。这仅仅是一个Region只有128M的情况下。如果是10G呢? 因此,通过计算,其实2层设计就可以满足集群的需求。因此在0.96版本以后就去掉了-ROOT-表了。
新的Region寻址方式
2层结构其实完全能满足业务的需求,因此0.96版本以后将-ROOT-表去掉了。如下图所示:
读分析
client要读取信息,先查询下Client端的Cache中是否存在数据,如果存在,刚直接返回数据。如果不存在,则进入到Zookeeper,查找到里面的相应数据存在的ROOT表中的地址。通过数据存在ROOT表中地址找到META,最终找到HRegion。找到HRegion后,它会先访问MemStore中是否存在数据,如果存在,则直接读取。如果没有就到BlockCache中查,如果还没有就再到HFile中查找数据,并将数据放到MemStore。
写分析
由于Hbase中默认的刷写方式是隐式刷写,所以你在put()数据时,它会自动保存到HRegion上,但当你批量处理数据时,它会将数据先保存到client端的Cache中。当你关闭隐式刷写时,你put()的数据则会保存到client Cache中,直到你调用刷写命令时,才会保存到HRegion中。即,HTable.setAutoFlush(false)
要写入的数据会先写到MemStore和HLog中,HMemcache建立缓存,HLog同步MemStore和HStore的事务日志,发起Flush Cache时,数据持久化到HStore中,并清空MemStore。
MemStore刷盘时机
(1)Heap百分比
这个全局的参数是控制内存整体的使用情况,当所有memstore占整个heap的最大比例的时候,会触发刷盘的操作。这个参数是hbase.regionserver.global.memstore.upperLimit,默认为整个heap内存的40%。但这并不意味着全局内存触发的刷盘操作会将所有的MemStore都进行输盘,而是通过另外一个参数hbase.regionserver.global.memstore.lowerLimit来控制,默认是整个heap内存的35%。当flush到所有memstore占整个heap内存的比率为35%的时候,就停止刷盘。这么做主要是为了减少刷盘对业务带来的影响,实现平滑系统负载的目的。
(2)MemStore上限
当MemStore的大小达到hbase.hregion.memstore.flush.size大小的时候会触发刷盘,默认128M大小。
(3)Hlog数量上限
前面说到Hlog为了保证Hbase数据的一致性,那么如果Hlog太多的话,会导致故障恢复的时间太长,因此Hbase会对Hlog的最大个数做限制。当达到Hlog的最大个数的时候,会强制刷盘。这个参数是hase.regionserver.max.logs,默认是32个。
(4)手工触发
可以通过hbase shell或者java api手工触发flush的操作。
(5)关闭RegionServer触发
在正常关闭RegionServer会触发刷盘的操作,全部数据刷盘后就不需要再使用Hlog恢复数据。
(6)Region使用HLOG恢复完数据后触发
当RegionServer出现故障的时候,其上面的Region会迁移到其他正常的RegionServer上,在恢复完Region的数据后,会触发刷盘,当刷盘完成后才会提供给业务访问。
Region的拆分
Hbase Region的拆分策略有比较多,比如除了3种默认过的策略,还有DelimitedKeyPrefixRegionSplitPolicy、KeyPrefixRegionSplitPolicy、DisableSplitPolicy等策略,这里只介绍3种默认的策略。分别是ConstantSizeRegionSplitPolicy策略、IncreasingToUpperBoundRegionSplitPolicy策略和SteppingSplitPolicy策略。
1.ConstantSizeRegionSplitPolicy
ConstantSizeRegionSplitPolicy策略是0.94版本之前的默认拆分策略,这个策略的拆分规则是:当region大小达到hbase.hregion.max.filesize(默认10G)后拆分。
这种拆分策略对于小表不太友好,按照默认的设置,如果1个表的Hfile小于10G就一直不会拆分。注意10G是压缩后的大小,如果使用了压缩的话。
如果1个表一直不拆分,访问量小也不会有问题,但是如果这个表访问量比较大的话,就比较容易出现性能问题。这个时候只能手工进行拆分。还是很不方便。
2.IncreasingToUpperBoundRegionSplitPolicy
IncreasingToUpperBoundRegionSplitPolicy策略是Hbase的0.94~2.0版本默认的拆分策略,这个策略相较于ConstantSizeRegionSplitPolicy策略做了一些优化,该策略的算法为:min(r^2*flushSize,maxFileSize ),最大为maxFileSize 。
从这个算是我们可以得出flushsize为128M、maxFileSize为10G的情况下,可以计算出Region的分裂情况如下:
第一次拆分大小为:min(10G,1*1*128M)=128M
第二次拆分大小为:min(10G,3*3*128M)=1152M
第三次拆分大小为:min(10G,5*5*128M)=3200M
第四次拆分大小为:min(10G,7*7*128M)=6272M
第五次拆分大小为:min(10G,9*9*128M)=10G
第六次拆分大小为:min(10G,11*11*128M)=10G
从上面的计算我们可以看到这种策略能够自适应大表和小表,但是这种策略会导致小表产生比较多的小region,对于小表还是不是很完美。
3.SteppingSplitPolicy
SteppingSplitPolicy是在Hbase 2.0版本后的默认策略,拆分规则为:If region=1 then:flush size * 2 else: MaxRegionFileSize。
还是以flushsize为128M、maxFileSize为10场景为列,计算出Region的分裂情况如下:
第一次拆分大小为:2*128M=256M
第二次拆分大小为:10G
从上面的计算我们可以看出,这种策略兼顾了ConstantSizeRegionSplitPolicy策略和IncreasingToUpperBoundRegionSplitPolicy策略,对于小表也比较好的适配。
Region拆分流程
从上图我们可以看出Region切分的详细流程如下:
(1) 第1步会在ZK的/hbase/region-in-transition/region-name下创建一个znode,并设置状态为SPLITTING
(2) 第2步master通过watch节点检测到Region状态的变化,并修改内存中Region状态的变化
(3) 第3步RegionServer在父Region的目录下创建一个名称为.splits的子目录
(4) 第4步RegionServer关闭父Region,强制将数据刷新到磁盘,并这个Region标记为offline的状态。此时,落到这个Region的请求都会返回NotServingRegionException这个错误
(5) 第5步RegionServer在.splits创建daughterA和daughterB,并在文件夹中创建对应的reference文件,指向父Region的Region文件
(6) 第6步RegionServer在HDFS中创建daughterA和daughterB的Region目录,并将reference文件移动到对应的Region目录中
(7) 第7步在.META.表中设置父Region为offline状态,不再提供服务,并将父Region的daughterA和daughterB的Region添加到.META.表中,已表名父Region被拆分成了daughterA和daughterB两个Region
(8) 第8步RegionServer并行开启两个子Region,并正式提供对外写服务
(9) 第9步RegionSever将daughterA和daughterB添加到.META.表中,这样就可以从.META.找到子Region,并可以对子Region进行访问了
(10) 第10步RegionServr修改/hbase/region-in-transition/region-name的znode的状态为SPLIT
备注:为了减少对业务的影响,Region的拆分并不涉及到数据迁移的操作,而只是创建了对父Region的指向。只有在做大合并的时候,才会将数据进行迁移。
那么通过reference文件如何才能查找到对应的数据呢?如下图所示:
Region合并流程
Region的合并分为小合并和大合并,下面就分别来做介绍:
1.小合并(MinorCompaction)
由前面的刷盘部分的介绍,我们知道当MemStore达到hbase.hregion.memstore.flush.size大小的时候会将数据刷到磁盘,生产StoreFile,因此势必产生很多的小问题,对于Hbase的读取,如果要扫描大量的小文件,会导致性能很差,因此需要将这些小文件合并成大一点的文件。因此所谓的小合并,就是把多个小的StoreFile组合在一起,形成一个较大的StoreFile,通常是累积到3个Store File后执行。通过参数hbase.hstore,compactionThreadhold配置。小合并的大致步骤为:
[1] 分别读取出待合并的StoreFile文件的KeyValues,并顺序地写入到位于./tmp目录下的临时文件中
[2] 将临时文件移动到对应的Region目录中
[3] 将合并的输入文件路径和输出路径封装成KeyValues写入WAL日志,并打上compaction标记,最后强制自行sync
[4] 将对应region数据目录下的合并的输入文件全部删除,合并完成
[5] 这种小合并一般速度很快,对业务的影响也比较小。本质上,小合并就是使用短时间的IO消耗以及带宽消耗换取后续查询的低延迟。
2.大合并(MajorCompaction)
所谓的大合并,就是将一个Region下的所有StoreFile合并成一个StoreFile文件,在大合并的过程中,之前删除的行和过期的版本都会被删除,拆分的母Region的数据也会迁移到拆分后的子Region上。大合并一般一周做一次,控制参数为hbase.hregion.majorcompaction。大合并的影响一般比较大,尽量避免统一时间多个Region进行合并,因此Hbase通过一些参数来进行控制,用于防止多个Region同时进行大合并。该参数为:hbase.hregion.majorcompaction.jitter
具体算法为:
hbase.hregion.majorcompaction参数的值乘于一个随机分数,这个随机分数不能超过hbase.hregion.majorcompaction.jitter的值。hbase.hregion.majorcompaction.jitter的值默认为0.5。
通过hbase.hregion.majorcompaction参数的值加上或减去hbase.hregion.majorcompaction参数的值乘于一个随机分数的值就确定下一次大合并的时间区间。
用户如果想禁用major compaction,只需要将参数hbase.hregion.majorcompaction设为0。建议禁用。
特点
- 大表:普通计算机处理10亿条数据(数十亿行*数百万列*数千个版本 = TB级或PB级的存储)
- 可伸缩:Hbase的伸缩性主要体现在两个方面,一个是基于上层处理能力(RegionServer)的伸缩,一个是基于存储的伸缩(HDFS)。
- 列式存储:这里的列指的是列族,面向列族的存储和权限控制,列族独立检索。列族固定,列可以动态扩展。
- 稀疏:对于为空(null)的列,并不占用存储空间,因此,表可以设计的非常稀疏。
- 多版本:每个单元中的数据可以有多个版本,默认情况下版本号自动分配,版本号是单元格插入时的时间戳;
- 数据类型单一:Hbase中的数据都是字符串
适用场景
(1) 数据量大(百T、PB级别)
(2) 查询简单(基于rowkey或者rowkey范围查询)
(3) 不涉及到复杂的关联
有几个典型的场景特别适合使用Hbase来存储:
(1) 海量订单流水数据(长久保存)
(2) 交易记录
(3) 数据库历史数据