Hbase——Region拆分与合并
一、Hbase的Region介绍
Region类似于数据库的分片和分区的概念,每个Region负责一小部分Rowkey范围的数据的读写和维护,Region包含了对应的起始行到结束行的所有信息。master将对应的region分配给不同的RergionServer,由RegionSever来提供Region的读写服务和相关的管理工作。
这部分主要介绍Region实例以及Region的寻找路径:
1.1 region实例
上图模拟了一个Hbase的表是如何拆分成region,以及分配到不同的RegionServer中去。上面是1个Userinfo表,里面有7条记录,其中rowkey为0001到0002的记录被分配到了Region1上,Rowkey为0003到0004的记录被分配到了Region2上,而rowkey为0005、0006和0007的记录则被分配到了Region3上。region1和region2被master分配给了RegionServer1(RS1),Region3被master配分给了RegionServer2(RS2)
备注:这里只是为了更容易的说明拆分的规则,其实真实的场景并不会几条记录拆分到不通的Region上,而是到一定的数据量才会拆分,具体的在Region的拆分那部分再具体的介绍。
1.2 Region的寻址
既然读写都在RegionServer上发生,每个RegionSever为一定数量的region服务,那么client要对某一行数据做读写的时候如何能知道具体要去访问哪个RegionServer呢?那就是接下来我们要讨论的问题。
1.2.1 老的Region寻址方式
在Hbase 0.96版本以前,Hbase有两个特殊的表,分别是-ROOT-表和.META.表,其中-ROOT-的位置存储在ZooKeeper中,-ROOT-本身存储了 .META. Table的RegionInfo信息,并且-ROOT-不会分裂,只有一个region。而.META.表可以被切分成多个region。读取的流程如下图所示:
-
第1步:client请求ZK获得-ROOT-所在的RegionServer地址
-
第2步:client请求-ROOT-所在的RS地址,获取.META.表的地址,client会将-ROOT-的相关信息cache下来,以便下一次快速访问
-
第3步:client请求 .META.表的RS地址,获取访问数据所在RegionServer的地址,client会将.META.的相关信息cache下来,以便下一次快速访问
-
第4步:client请求访问数据所在RegionServer的地址,获取对应的数据
从上面的路径我们可以看出,用户需要3次请求才能直到用户Table真正的位置,这在一定程序带来了性能的下降。在0.96之前使用3层设计的主要原因是考虑到元数据可能需要很大。但是真正集群运行,元数据的大小其实很容易计算出来。在BigTable的论文中,每行METADATA数据存储大小为1KB左右,如果按照一个Region为128M的计算,3层设计可以支持的Region个数为234个,采用2层设计可以支持217(131072)。那么2层设计的情况下一个 集群可以存储4P的数据。这仅仅是一个Region只有128M的情况下。如果是10G呢? 因此,通过计算,其实2层设计就可以满足集群的需求。因此在0.96版本以后就去掉了-ROOT-表了。
1.2.2 新的Region寻址方式
如上面的计算,2层结构其实完全能满足业务的需求,因此0.96版本以后将-ROOT-表去掉了。如下图所示:
访问路径变成了3步:
-
第1步:Client请求ZK获取.META.所在的RegionServer的地址。
-
第2步:Client请求.META.所在的RegionServer获取访问数据所在的RegionServer地址,client会将.META.的相关信息cache下来,以便下一次快速访问。
-
第3步:Client请求数据所在的RegionServer,获取所需要的数据。
总结去掉-ROOT-的原因有如下2点:
- 1、提高性能。
- 2、2层结构已经足以满足集群的需求。
注意:
- Client会缓存.META.的数据,用来加快访问。
- Client的元数据缓存没更新,当.META.的数据发生更新。Client再次根据缓存去访问的时候,会出现错误,当出现异常达到重试次数后就会去.META.所在的RegionServer获取最新的数据,如果.META.所在的RegionServer也变了,Client就会去ZK上获取.META.所在的RegionServer的最新地址。
二、Region的拆分
2.1 Hbase Region的自动拆分策略
Hbase Region的拆分策略有比较多,比如除了3种默认过的策略,还有DelimitedKeyPrefixRegionSplitPolicy、KeyPrefixRegionSplitPolicy、DisableSplitPolicy等策略,这里着重介绍3种默认的策略。分别是ConstantSizeRegionSplitPolicy策略、IncreasingToUpperBoundRegionSplitPolicy策略和SteppingSplitPolicy策略。
KeyPrefixRegionSplitPolicy
根据rowKey的前缀对数据进行分组,这里是指定rowKey的前多少位作为前缀,比如rowKey都是16位的,指定前5位是前缀,那么前5位相同的rowKey在进行region split的时候会分 到相同的region中。
DelimitedKeyPrefixRegionSplitPolicy
保证相同前缀的数据在同一个region中,例如rowKey的格式为:userid_eventtype_eventid,指定的delimiter为 _ ,则split的的时候会确保userid相同的数据在同一个 region中。
DisabledRegionSplitPolicy
不启用自动拆分, 需要指定手动拆分
ConstantSizeRegionSplitPolicy
固定大小拆分策略:ConstantSizeRegionSplitPolicy策略是0.94版本之前的默认拆分策略。
这个策略的拆分规则是:当region大小达到hbase.hregion.max.filesize(默认10G)后拆分。 这种拆分策略对于小表不太友好,按照默认的设置,如果1个表的Hfile小于10G就一直不会拆分。注意10G是压缩后的大小,如果使用了压缩的话。
如果1个表一直不拆分,访问量小也不会有问题,但是如果这个表访问量比较大的话,就比较容易出现性能问题。这个时候只能手工进行拆分。还是很不方便。
唯一的参数是(hbase-site.xml):
# hbase.hregion.max.filesize:region最大大小,默认为10GB
<property>
<name>hbase.hregion.max.filesize</name>
<value>10 * 1024 * 1024 * 1024</value>
</property>
IncreasingToUpperBoundRegionSplitPolicy
动态限制拆分策略:IncreasingToUpperBoundRegionSplitPolicy策略是Hbase的0.94~2.0版本默认的拆分策略,这个策略相较于ConstantSizeRegionSplitPolicy策略做了一些优化,该策略的算法为:min(r^2*flushSize,maxFileSize ),最大为maxFileSize 。
从这个算是我们可以得出flushsize为128M、maxFileSize为10G的情况下,可以计算出Region的分裂情况如下:
- 第一次拆分大小为:min(10G,11128M)=128M
- 第二次拆分大小为:min(10G,33128M)=1152M
- 第三次拆分大小为:min(10G,55128M)=3200M
- 第四次拆分大小为:min(10G,77128M)=6272M
- 第五次拆分大小为:min(10G,99128M)=10G
- 第六次拆分大小为:min(10G,1111128M)=10G
从上面的计算我们可以看到这种策略能够自适应大表和小表,但是这种策略会导致小表产生比较多的小region,对于小表还是不是很完美。
涉及到的相关配置有(hbase-site.xml中配置):
#动态限制拆分策略的初始化大小
hbase.increasing.policy.initial.size
#memstore最大刷写值
hbase.hregion.memstore.flush.size
#固定大小拆分策略定义的最大Region大小
hbase.hregion.max.filesize
SteppingSplitPolicy
SteppingSplitPolicy是在Hbase 2.0版本后的默认策略,,拆分规则为:If region=1 then: flush size * 2 else: MaxRegionFileSize。
If region=1
then: flush size * 2
else: MaxRegionFileSize。
还是以flush size为128M、maxFileSize为10场景为列,计算出Region的分裂情况如下: 第一次拆分大小为:2*128M=256M 第二次拆分大小为:10G
从上面的计算我们可以看出,这种策略兼顾了ConstantSizeRegionSplitPolicy策略和IncreasingToUpperBoundRegionSplitPolicy策略,对于小表也肯呢个比较好的适配。
2.2 Hbase Region拆分的详细流程
Hbase的详细拆分流程图如下:
从上图我们可以看出Region切分的详细流程如下:
-
1、在ZK的/hbase/region-in-transition/region-name下创建一个znode,并设置状态为SPLITTING
-
2、master通过watch节点检测到Region状态的变化,并修改内存中Region状态的变化
-
3、RegionServer在父Region的目录下创建一个名称为.splits的子目录
-
4、RegionServer关闭父Region,强制将数据刷新到磁盘,并这个Region标记为offline的状态。此时,落到这个Region的请求都会返回NotServingRegionException这个错误
-
5、RegionServer在.splits创建daughterA和daughterB,并在文件夹中创建对应的reference文件,指向父Region的Region文件
-
6、RegionServer在HDFS中创建daughterA和daughterB的Region目录,并将reference文件移动到对应的Region目录中
-
7、在.META.表中设置父Region为offline状态,不再提供服务,并将父Region的daughterA和daughterB的Region添加到.META.表中,已表名父Region被拆分成了daughterA和daughterB两个Region
-
8、RegionServer并行开启两个子Region,并正式提供对外写服务
-
9、RegionSever将daughterA和daughterB添加到.META.表中,这样就可以从.META.找到子Region,并可以对子Region进行访问了
-
10、RegionServr修改/hbase/region-in-transition/region-name的znode的状态为SPLIT
注意:为了减少对业务的影响,Region的拆分并不涉及到数据迁移的操作,而只是创建了对父Region的指向。只有在做大合并的时候,才会将数据进行迁移。
那么通过reference文件如何才能查找到对应的数据呢?如下图所示:
- 根据文件名来判断是否是reference文件。
- 由于reference文件的命名规则为前半部分为父Region对应的File的文件名,后半部分是父Region的名称,因此读取的时候也根据前半部分和后半部分来识别。
- 根据reference文件的内容来确定扫描的范围,reference的内容包含两部分,一部分是切分点splitkey,另一部分是boolean类型的变量(true或者false)。如果为true则扫描文件的上半部分,false则扫描文件的下半部分。
- 接下来确定了扫描的文件,以及文件的扫描范围,那就按照正常的文件检索了。
2.2 Hbase Region的手动拆分策略
pre-splitting Region预拆分
就是在建表的时候就定义好拆分点的算法,使用org.apache.hadoop.hbase.util.RegionSplitter类来创建表,并传入拆分点算法,就可以在建表同事定义拆分点算法。比如;
hbase org.apache.hadoop.hbase.util.RegionSplitter table_name HexStringSplit -c 10 -f mycf
HexStringSplit:指定的拆分点算法
-c:要拆分的Region数量
-f:要建立的列族名称
#会建立10个固定的Region,使用如下语句查看创建的Region
scan 'hbase:meta',{STARTROW=>'table_name',LIMIT => 10}
具体的拆分点算法有:
-
HexStringSplit
ASCII码预拆分策略,只需要传入一个要拆分的Region的数量,HexStringSplit会将数据从“00000000”到“FFFFFFFF”之间的数据长度按照n等分之后算出每一段的其实rowkey和结束rowkey,以此作为拆分点。 -
UniformSplit
字节码预拆分策略,与ASCII码预拆分不同的是,起始结束不是Sting而是byte[] -
起始rowkey是ArrayUtils.EMPTY_BYTE_ARRAY
-
结束rowkey是new byte[]{xFF,xFF,xFF,xFF,xFF,xFF,xFF,xFF}
最后调用Bytes.split方法把其实rowkey到结束rowkey之间的长度n等分,然后取每一段的起始和结束作为拆分点
默认预拆分算法只有这两个,我们也可以通过实现SplitAlgorithm接口实现自己的拆分算法,或者干脆手动定出拆分点。
-
手动制定拆分点(属于预拆分)
只需要在建表的时候跟上SPLITS参数:
create 'test_split2','mycf2','mysf2',SPLITS=>['AAA','BBB','CCC']
-
强制拆分
其实这个才是实际意义上的手动拆分,通过运行命令强制手动拆分(forced splits),调用hbase shell的split方法。
#将表table_name从1000出拆分为两个Region
split 'table_name,c,1476405886999.96dd83893d683','1000'
#其他调用方式有:
split 'tableName'
split 'namespace:tableName'
split 'regionName'#format:'tableName,startKey,id'
split 'tableName','splitKey'
split 'regionName','splitKey'
总结
- 建议开始的时候定义预拆分,导入初始数据,之后使用自动拆分来让HBase自动管理Region。不要关闭自动拆分。这样科比避免因为直接使用预拆分导致的热点Region问题。
- 尽量使用适合业务的拆分策略,比如不要在时间戳为rowkey前缀的情况下还是用KeyPrefixRegionSplitPolicy来作为拆分策略,这会导致严重的热点问题。
三、Region的合并
首先,Region的合并(merge)并不是为了性能考虑而是处于维护的目的被创造出来的。比如删了大量的数据,导致每个Region都变小了,这个时候合并Region就比较合适了。
3.1 通过Merge类冷合并Region
通过org.apache.hadoop.hbase.util.Merge类来实现,不需要进入hbase shell,直接执行:
hbase org.apache.hadoop.hbase.util.Merge table_name \
table_name,a,147608089478.39erijidsfd8s098fen32j3i8d9. \
table_name,b,148893879502.48jfidnxoskd023843257822j3i.
就可以实现两个Region的合并,但是有一个前提,必须保证这两个Region已经下线,保证HMaster和所有的HRegionServer都停掉,否则会报错,但是这样太麻烦了,而且不适合生产使用。
3.2 通过online_merge热合并Region
与冷合并不同的是,online_merge的传参是Region的hash值,而Region的hash值就是Region名称的最后那段在两个.之间的字符串部分,需要进入hbase shell:
> merge_region '39erijidsfd8s098fen32j3i8d9','48jfidnxoskd023843257822j3i'
#通过hbase:meta查看Region合并后的信息
3.3 HFile合并(Compact)
MemStore每次刷写都会生成一个HFile,当HFile变多,回到值读取数据磁头寻址缓慢,因为HFile都分散在不同的位置,为了防止寻址动作过多,适当的减少碎片文件,就需要合并HFile。
合并操作主要是在一个Store里边找到需要合并的HFile,然后把它们合并起来,合并在大体意义上有两大类Minor Compation和Major Compaction:
- Minor Compaction:将Store中多个HFile合并为一个HFile,这个过程中,达到TTL(记录保留时间)会被移除,但是有墓碑标记的记录不会被移除,因为墓碑标记可能存储在不同HFile中,合并可能会跨国部分墓碑标记。这种合并的触发频率很高
- Major Compaction:合并Store中所有的HFile为一个HFile(并不是把一个Region中的HFile合并为一个),这个过程有墓碑标记的几率会被真正移除,同时超过单元格maxVersion的版本记录也会被删除。合并频率比较低,默认7天执行一次,并且性能消耗非常大,最后手动控制进行合并,防止出现在业务高峰期。
注意:有资料说只有Major合并才会删数据,其实Major合并删除的是带墓碑标记的,而Minor合并直接就不读取TTL过期文件,所以也相当于删除了。
小合并(MinorCompaction)
由前面的刷盘部分的介绍,我们知道当MemStore达到hbase.hregion.memstore.flush.size大小的时候会将数据刷到磁盘,生产StoreFile,因此势必产生很多的小问题,对于Hbase的读取,如果要扫描大量的小文件,会导致性能很差,因此需要将这些小文件合并成大一点的文件。因此所谓的小合并,就是把多个小的StoreFile组合在一起,形成一个较大的StoreFile,通常是累积到3个Store File后执行。通过参数hbase.hstore,compactionThreadhold配置。小合并的大致步骤为:
- 分别读取出待合并的StoreFile文件的KeyValues,并顺序地写入到位于./tmp目录下的临时文件中。
- 将临时文件移动到对应的Region目录中。
- 将合并的输入文件路径和输出路径封装成KeyValues写入WAL日志,并打上compaction标记,最后强制自行sync。
- 将对应region数据目录下的合并的输入文件全部删除,合并完成。
这种小合并一般速度很快,对业务的影响也比较小。本质上,小合并就是使用短时间的IO消耗以及带宽消耗换取后续查询的低延迟。
大合并(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。建议禁用。
参考:
https://blog.csdn.net/asd136912/article/details/101168177