Region分裂

Region分裂是HBase的核心功能之一,它是HBase实现分布式可扩展性的基础。在HBase中,Region分裂有多种触发策略可以配置,一旦触发分裂,HBase就会寻找分裂点,然后执行真正的分裂操作。

一、What

HBase支持好几种分裂触发策略。每种触发策略都有其各自的适用场景,我们可以根据业务在表级别选择不同的分裂触发策略。以下是几种常用的分裂触发策略:

  • ConstantSizeRegionSplitPolicy:0.94版本之前默认分裂策略——当某个Region中最大的Store的大小超过阈值(hbase.hregion.max.filesize)后触发分裂。相比于其他策略,该策略最简单,但是在生产环境这种分裂策略却有很大弊端——对于大表和小表没有明显的区分。如果阈值设置较大对大表比较友好,但是小表有可能不会触发分裂,极端情况下可能就只有1个Region,这对业务来说并不是什么好事。如果阈值设置较小则对小表友好,但一个大表就会在整个集群中产生大量的Region,这对于集群的管理、资源使用来说都不是一件好事。

  • IncreasingToUpperBoundRegionSplitPolicy:0.94版本~2.0版本默认分裂策略。该策略总体来看和ConstantSizeRegionSplitPolicy思路相同,某个Region中最大Store大小超过设置阈值就会触发分裂。但是这个阈值并不是一个固定的值,而是在一定条件下不断调整,调整后的阈值大小和Region所属表在当前RegionServer上的Region个数有关系,调整后的阈值等于(#regions) * (#regions) * (#regions) * flush size * 2,当然阈值并不会无限增大,最大值为用户设置的MaxRegionFileSize。该策略很好地弥补了ConstantSizeRegionSplitPolicy的短板,能够自适应大表和小表,而且在集群规模较大的场景下,对很多大表来说表现很优秀。然而,这种策略并不完美,比如在大集群场景下,很多小表就会产生大量小Region,分散在整个集群中。

  • SteppingSplitPolicy:2.0版本的默认分裂策略。这种分裂策略的分裂阈值也发生了变化,相比IncreasingToUpperBoundRegionSplitPolicy简单了一些,分裂阈值大小和待分裂Region所属表在当前RegionServer上的Region个数有关,如果Region个数等于1,分裂阈值为flush size * 2,否则为MaxRegionFileSize。这种分裂策略对于大集群中的大表、小表会比IncreasingToUpperBoundRegionSplitPolicy更加友好,小表不会再产生大量的小Region。

此外,还有一些其他分裂策略。比如,使用DisableSplitPolicy策略可以禁止Region分裂;而KeyPrefixRegionSplitPolicy和DelimitedKeyPrefixRegionSplitPolicy依然依据默认的分裂策略,但对于分裂点有自己的规定,比如KeyPrefixRegionSplitPolicy要求必须让相同的PrefixKey处于同一个Region中。

在实际的选择中,使用默认的分裂策略即可,当然我们也可以根据需要在CF级别设置Region分裂策略,如下:

1
create 'table', {NAME => 'cf', SPLIT_POLICY => 'org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy'}

二、How

2.1、寻找分裂点

在满足Region分裂条件后就会触发Region分裂,而触发分裂后的第一步就是:寻找分裂点。所有的分裂策略,不论是ConstantSizeRegionSplitPolicy、IncreasingToUpperBoundRegionSplitPolicy还是SteppingSplitPolicy,对于分裂点的定义都是一致的。当然,我们手动执行分裂时指定分裂点的情况除外。

HBase对于分裂点的定义是:Region中最大Store中的最大文件中最中心的一个Block的首个rowkey。另外,HBase还规定,如果定位到的rowkey是整个文件的首个rowkey或者最后一个rowkey,则认为没有分裂点。

此时,我们可能会问什么情况下会出现没有分裂点的场景?

最典型的场景就是在待分裂的region中只有一个block,此时执行split就无法分裂。比如,新建一张测试表,然后往表中插入几条数据并执行flush,最后在执行split,就会发现数据表并没有真正执行分裂。原因就在于测试表中只有一个Block,此时如果查看debug日志会有如下内容:

1
2
3
2017-08-19 11:26:17,404 INFO [PriorityRpcServer.handler=1,queue=1,port=60020] regionserver.RSRpcServices: Splitting split,,1503112775843.6adfcc49ba04f307d6c6a604572d1bb2.
2017-08-19 11:26:17,499 DEBUG [PriorityRpcServer.handler=1,queue=1,port=60020] regionserver.StoreFile: cannot split because midkey is the same as first or last row
2017-08-19 11:26:17,499 DEBUG [PriorityRpcServer.handler=1,queue=1,port=60020] regionserver.CompactSplitThread: Region split,,1503112775843.6adfcc49ba04f307d6c6a604572d1bb2. not splittable because midkey=null

2.2、执行分裂

HBase将整个分裂过程包装成了一个事务,目的是保证分裂过程的原子性。这个事务的整个过程分为三个阶段:prepare、execute和rollback。我们可以通过如下伪代码来理解这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (!st.prepare()) return;

try {
st.execute(this.server, this.server, user);
status = true;
} catch (Exception e) {
try {
st.rollback(this.server, this.server);
} catch (IOException re) {
String msg = "Failed rollback of failed split parent.getRegionNameAsSring() -- aborting server";
LOG.info(msg, re);
}
}

2.2.1、prepare阶段

在内存中初始化两个子region,具体会生成两个HRegionInfo对象,包括tableName、regionName、startKey和endKey等。同时还会生成一个transaction journal对象,用来记录分裂的进展。

2.2.2、execute阶段

分裂的核心操作,如下所示:

这个阶段的步骤如下:

  • 1)RegionServer将ZooKeeper节点/region-in-transition中该Region的状态更改为SPLITING。

  • 2)Master通过watch节点/region-in-transition检测到Region状态改变,并修改内存中Region的状态。此时,在Master页面RIT模块可以看到Region执行split的状态信息。

  • 3)在父存储目录下新建临时文件夹.split,保存split后的daughter region信息。

  • 4)关闭父Region。父Region关闭数据写入并触发flush操作,将写入Region的数据全部持久化到磁盘。此后短时间内客户端落在父Region上的请求都会抛出异常NotServingRegionException。

  • 5)在.split文件夹下新建两个文件夹,称为daughter A、daughter B,并在文件夹中生成reference文件,分别指向父Region中对应文件。这个步骤是所有步骤中最核心的一个环节,会生成如下reference文件日志

1
2017-08-12 11:53:38,158 DEBUG [StoreOpener-0155388346c3c919d3f05d7188e885e0-1] regionserver.StoreFileInfo: reference 'hdfs:// hdfscluster/hbase-rsgroup/data/default/music/0155388346c3c919d3f05d7188e885e0/cf/d24415c4fb44427b8f698143e5c4d9dc.00bb6239169411e4d0ecb6ddfdbacf66' to region=00bb6239169411e4d0ecb6ddfdbacf66 hfile=d24415c4fb44427b8f698143e5c4d9dc。

其中,reference文件名为d24415c4fb44427b8f698143e5c4d9dc.00bb6239169411e4d0ecb6ddfdbacf66,格式比较特殊,该文件名具体含义如下:根据日志可以看到,分裂的父Region是00bb6239169411e4d0ecb6ddfdbacf66,对应的HFile文件是d24415c4fb44427b8f698143e5c4d9dc,可见通过reference文件名就可以知道referene文件指向哪个父Region中的哪个HFile文件,如下所示:

除此之外,reference文件的文件内容也非常重要。reference文件是一个引用文件(并非Linux链接文件),文件内容并不是用户数据,而是由两部分构成:其一是分裂点splitkey,其二是一个boolean类型的变量(true或者false),true表示该reference文件引用的是父文件的上半部分(top),false表示引用的是下半部分(bottom)。用户可以使用hadoop命令查看reference文件的具体内容:

1
hadoop dfs -cat /hbase-rsgroup/data/default/music/0155388346c3c919d3f05d7188e885e0/cf/d24415c4fb44427b8f698143e5c4d9dc.00bb6239169411e4d0ecb6ddfdbacf66
  • 6)父Region分裂为两个子Region后,将daughter A、daughter B拷贝到HBase根目录下,形成两个新的Region。

  • 7)父Region通知修改hbase:meta表后下线,不再提供服务。下线后父Region在meta表中的信息并不会马上删除,而是将split列、offline列标注为true,并记录两个子Region,如下:

  • 8)开启daughter A、daughter B两个子Region。通知修改hbase:meta表,正式对外提供服务,如下:

2.2.3、rollback阶段

如果execute阶段出现异常,则执行rollback操作。为了实现回滚,整个分裂过程分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。代码中使用JournalEntryType来表示各个子阶段,如下:

Jounal Entey Type 拆分进度 回滚(清理垃圾数据)
STARTED 拆分逻辑被触发 NONE
PREPARED 执行拆分前的准备工作 NONE
SET_SPLITING_IN_ZK Zookeeper中创建出拆分节点 删除Zookeeper中的拆分节点
CREATE_SPLIT_DIR 拆分目录.split被生成 清理父Region下的拆分目录
CLOSED_PARENT_REGION 父Region被关闭 重新对父Region执行初始化操作
OFFLINE_PARENT 父Region被下线,不再提供上线服务 重新将父Region上线
STARTED_REGION_A_CREATED 第一个子Region被成功创建 清理第一个子Region的存储目录
STARTED_REGION_B_CREATED 第二个子Region被成功创建 清理第二个子Region的存储目录
OPENED_REGION_A 第一个子Region被开启 关闭第一个子Region
OPENED_REGION_B 第二个子Region被开启 关闭第二个子Region

三、How Good

3.1 、原子性保证

Region分裂是一个比较复杂的过程,涉及父Region中HFile文件分裂、两个子Region生成、系统meta元数据更改等很多子步骤,因此必须保证整个分裂过程的原子性,即要么分裂成功,要么分裂失败,在任何情况下不能出现分裂完成一半的情况。

为了实现原子性,HBase使用状态机的方式保存分裂过程中每个子步骤状态,这样一旦出现异常,系统可以根据当前所处的状态决定是否回滚,以及如何回滚。遗憾的是,目前实现中这些中间状态都只存储在内存中,一旦在分裂过程中出现RegionServer宕机的情况,有可能会出现分裂处于中间状态的情况,也就是RIT状态。这种情况下需要使用HBCK工具具体查看并分析解决方案。

在2.0版本之后,HBase将实现新的分布式事务框架Procedure V2(HBASE-12459),新框架使用类似HLog的日志文件存储这种单机事务(DDL操作、split操作、move操作等)的中间状态,因此可以保证即使在事务执行过程中参与者发生了宕机,依然可以使用对应日志文件为协调者,对事务进行回滚操作或者重试提交,从而大大减少甚至杜绝RIT现象。这也是HBase 2.0在可用性方面最值得期待的一个亮点功能。

在执行split的过程中一旦发生RegionServer宕机等异常可能会导致region-in-transition。通常情况下建议使用HBCK查看报错信息,然后再根据HBCK提供的一些工具进行修复,HBCK提供了部分命令对处于split状态的rit region进行修复,主要的命令如下:

  • -fixSplitParents Try to force offline split parents to be online.

  • -removeParents Try to offline and sideline lingering parents an
    d keep daughter regions.

  • -fixReferenceFiles Try to offline lingering reference store files

3.2、对其他模块的影响

Region分裂过程因为没有涉及数据的移动,所以分裂成本本身并不是很高,可以很快完成。分裂后子Region的文件实际没有任何用户数据,文件中存储的仅是一些元数据信息——分裂点rowkey等。此时我们可能会有一些疑问:

通过reference文件如何查找数据呢?

子Region的数据实际在什么时候完成真正迁移?

数据迁移完成之后父Region什么时候会被删掉?

3.2.1、通过reference文件查找数据

通过reference文件查找数据的整个流程如下:

  • 根据reference文件名(父Region名 + HFile文件名)定位到真实数据所在文件路径。

  • 根据reference文件内容中记录的两个重要字段确定实际扫描范围。top字段表示扫描范围是HFile的上半部分还是下半部分。如果top为true,表示扫描的是上半部分,结合splitkey字段可以明确扫描范围为[firstkey,splitkey);如果top为false,表示扫描的是下半部分,结合splitkey字段可以明确扫描范围为[splitkey,endkey)。

3.2.2、父Region的数据迁移到子Region目录的时间

迁移发生在子Region执行Major Compaction时。根据Compaction原理,从一系列小文件中依次由小到大读出所有数据并写入一个大文件,完成之后再将所有小文件删掉,因此Compaction本身就是一次数据迁移。分裂后的数据迁移完全可以借助Compaction实现,子Region执行Major Compaction后会将父目录中属于该子Region的所有数据读出来,并写入子Region目录数据文件中。

3.3.3、父Region被删除的时间

Master会启动一个线程定期遍历检查所有处于splittig状态的父Region,确定父Region是否可以被清理。检查过程分为两步:

1)检测线程首先会在meta表中读出所有split列为true的Region,并加载出其分裂后生成的两个子Region(meta表中splitA列和splitB列)。

2)检查两个子Region是否还存在引用文件,如果都不存在引用文件就可以认为该父Region对应的文件可以被删除。

四、参考

[HBase原理与实践](HBase原理与实践 (豆瓣))


Region分裂
https://kuberxy.github.io/2022/04/07/Region分裂/
作者
Mr.x
发布于
2022年4月7日
许可协议