作者:范欣欣
Region 自动切分是 HBase 能够拥有良好扩张性的最重要因素之一,也必然是所有分布式系统追求无限扩展性的一副良药。HBase 系统中 Region 自动切分是如何实现的,这里面涉及很多知识点,比如 Region 切分的触发条件是什么、Region 切分的切分点在哪里、如何切分才能最大的保证 Region 的可用性、如何做好切分过程中的异常处理、切分过程中要不要将数据移动等,这篇文章将会对这些细节进行基本的说明,一方面可以让大家对 HBase 中 Region 自动切分有更加深入的理解,另一方面如果想实现类似的功能也可以参考 HBase 的实现方案。
Region 切分触发策略在最新稳定版(1.2.6)中,HBase 已经有多达 6 种切分触发策略。当然,每种触发策略都有各自的适用场景,用户可以根据业务在表级别选择不同的切分触发策略。常见的切分策略如下图:
ConstantSizeRegionSplitPolicy:0.94 版本前默认切分策略。这是最容易理解但也最容易产生误解的切分策略,从字面意思来看,当 region 大小大于某个阈值(hbase.hregion.max.filesize)之后就会触发切分,实际上并不是这样,真正实现中这个阈值是对于某个 store 来说的,即一个 region 中最大 store 的大小大于设置阈值之后才会触发切分。
另外一个大家比较关心的问题是这里所说的 store 大小是压缩后的文件总大小还是未压缩文件总大小,实际实现中 store 大小为压缩后的文件大小(采用压缩的场景)。ConstantSizeRegionSplitPolicy 相对来来说最容易想到,但是在生产线上这种切分策略却有相当大的弊端:切分策略对于大表和小表没有明显的区分。阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触发分裂,极端情况下可能就 1 个,这对业务来说并不是什么好事。如果设置较小则对小表友好,但一个大表就会在整个集群产生大量的 region,这对于集群的管理、资源使用、failover 来说都不是一件好事。
IncreasingToUpperBoundRegionSplitPolicy: 0.94 版本~ 2.0 版本默认切分策略。这种切分策略微微有些复杂,总体来看和 ConstantSizeRegionSplitPolicy 思路相同,一个 region 中最大 store 大小大于设置阈值就会触发切分。但是这个阈值并不像 ConstantSizeRegionSplitPolicy 是一个固定的值,而是会在一定条件下不断调整,调整规则和 region 所属表在当前 regionserver 上的 region 个数有关系 :(#regions) * (#regions) * (#regions) * flush size * 2,当然阈值并不会无限增大,最大值为用户设置的 MaxRegionFileSize。
这种切分策略很好地弥补了 ConstantSizeRegionSplitPolicy 的短板,能够自适应大表和小表。而且在大集群条件下对于很多大表来说表现很优秀,但并不完美,这种策略下很多小表会在大集群中产生大量小 region,分散在整个集群中。而且在发生 region 迁移时也可能会触发 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 切分策略,命令为:
- create 'table', {NAME => 'cf', SPLIT_POLICY => 'org.apache.hadoop.hbase.regionserver. ConstantSizeRegionSplitPolicy'}
region 切分策略会触发 region 切分,切分开始之后的第一件事是寻找切分点-splitpoint。所有默认切分策略,无论是 ConstantSizeRegionSplitPolicy、IncreasingToUpperBoundRegionSplitPolicy 抑或是 SteppingSplitPolicy,对于切分点的定义都是一致的。当然,用户手动执行切分时是可以指定切分点进行切分的,这里并不讨论这种情况。
那切分点是如何定位呢?整个 region 中最大 store 中的最大文件中最中心的一个 block 的首个 rowkey。这是一句比较消耗脑力的语句,需要细细品味。另外,HBase 还规定,如果定位到的 rowkey 是整个文件的首个 rowkey 或者最后一个 rowkey 的话,就认为没有切分点。
什么情况下会出现没有切分点的场景呢?最常见的就是一个文件只有一个 block,执行 split 的时候就会发现无法切分。很多新同学在测试 split 的时候往往都是新建一张新表,然后往新表中插入几条数据并执行一下 flush,再执行 split,奇迹般地发现数据表并没有真正执行切分。原因就在这里,这个时候仔细的话你翻看 debug 日志是可以看到这样的日志滴:
HBase 将整个切分过程包装成了一个事务,意图能够保证切分事务的原子性。整个分裂事务过程分为三个阶段:prepare – execute – (rollback) ,操作模版如下:
1、regionserver 更改 ZK 节点 /region-in-transition 中该 region 的状态为 SPLITING。
2、master 通过 watch 节点 / region-in-transition 检测到 region 状态改变,并修改内存中 region 的状态,在 master 页面 RIT 模块就可以看到 region 执行 split 的状态信息。
3、在父存储目录下新建临时文件夹. split 保存 split 后的 daughter region 信息。
4、关闭 parent region:parent region 关闭数据写入并触发 flush 操作,将写入 region 的数据全部持久化到磁盘。此后短时间内客户端落在父 region 上的请求都会抛出异常 NotServingRegionException。
5、核心分裂步骤:在. split 文件夹下新建两个子文件夹,称之为 daughter A、daughter B,并在文件夹中生成 reference 文件,分别指向父 region 中对应文件。这个步骤是所有步骤中最核心的一个环节,生成 reference 文件日志如下所示:
其中 reference 文件名为
d24415c4fb44427b8f698143e5c4d9dc.00bb6239169411e4d0ecb6ddfdbacf66,格式看起来比较特殊,那这种文件名具体什么含义呢?那来看看该 reference 文件指向的父 region 文件,根据日志可以看到,切分的父 region 是 00bb6239169411e4d0ecb6ddfdbacf66,对应的切分文件是 d24415c4fb44427b8f698143e5c4d9dc,可见 reference 文件名是个信息量很大的命名方式,如下所示:
除此之外,还需要关注 reference 文件的文件内容,reference 文件是一个引用文件(并非 linux 链接文件),文件内容很显然不是用户数据。文件内容其实非常简单,主要有两部分构成:其一是切分点 splitkey,其二是一个 boolean 类型的变量(true 或者 false),true 表示该 reference 文件引用的是父文件的上半部分(top),而 false 表示引用的是下半部分 (bottom)。为什么存储的是这两部分内容?且听下文分解。
看官可以使用 Hadoop 命令亲自来查看 reference 文件的具体内容:
6. 父 region 分裂为两个子 region 后,将 daughter A、daughter B 拷贝到 HBase 根目录下,形成两个新的 region。
7. parent region 通知修改 hbase.meta 表后下线,不再提供服务。下线后 parent region 在 meta 表中的信息并不会马上删除,而是标注 split 列、offline 列为 true,并记录两个子 region。为什么不立马删除?且听下文分解。
8. 开启 daughter A、daughter B 两个子 region。通知修改 hbase.meta 表,正式对外提供服务。
rollback 阶段:如果 execute 阶段出现异常,则执行 rollback 操作。为了实现回滚,整个切分过程被分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。代码中使用 JournalEntryType 来表征各个子阶段,具体见下图:
整个 region 切分是一个比较复杂的过程,涉及到父 region 中 HFile 文件的切分、两个子 region 的生成、系统 meta 元数据的更改等很多子步骤,因此必须保证整个切分过程的事务性,即要么切分完全成功,要么切分完全未开始,在任何情况下也不能出现切分只完成一半的情况。
为了实现事务性,HBase 设计了使用状态机(见 SplitTransaction 类)的方式保存切分过程中的每个子步骤状态,这样一旦出现异常,系统可以根据当前所处的状态决定是否回滚,以及如何回滚。遗憾的是,目前实现中这些中间状态都只存储在内存中,因此一旦在切分过程中出现 regionserver 宕机的情况,有可能会出现切分处于中间状态的情况,也就是 RIT 状态。这种情况下需要使用 hbck 工具进行具体查看并分析解决方案。在 2.0 版本之后,HBase 实现了新的分布式事务框架 Procedure V2(HBASE-12439),新框架将会使用 HLog 存储这种单机事务(DDL 操作、Split 操作、Move 操作等)的中间状态,因此可以保证即使在事务执行过程中参与者发生了宕机,依然可以使用 HLog 作为协调者对事务进行回滚操作或者重试提交,大大减少甚至杜绝 RIT 现象。这也是是 2.0 在可用性方面最值得期待的一个亮点!
Region 切分对其它模块的影响通过 region 切分流程的了解,我们知道整个 region 切分过程并没有涉及数据的移动,所以切分成本本身并不是很高,可以很快完成。切分后子 region 的文件实际没有任何用户数据,文件中存储的仅是一些元数据信息-切分点 rowkey 等,那通过引用文件如何查找数据呢?子 region 的数据实际在什么时候完成真正迁移?数据迁移完成之后父 region 什么时候会被删掉?
1. 通过 reference 文件如何查找数据?
这里就会看到 reference 文件名、文件内容的实际意义啦。整个流程如下图所示:
2. 父 region 的数据什么时候会迁移到子 region 目录?
答案是子 region 发生 major_compaction 时。我们知道 compaction 的执行实际上是将 store 中所有小文件一个 KV 一个 KV 从小到大读出来之后再顺序写入一个大文件,完成之后再将小文件删掉,因此 compaction 本身就需要读取并写入大量数据。子 region 执行 major_compaction 后会将父目录中属于该子 region 的所有数据读出来并写入子 region 目录数据文件中。可见将数据迁移放到 compaction 这个阶段来做,是一件顺便的事。
3. 父 region 什么时候会被删除?
实际上 HMaster 会启动一个线程定期遍历检查所有处于 splitting 状态的父 region,确定检查父 region 是否可以被清理。检测线程首先会在 meta 表中揪出所有 split 列为 true 的 region,并加载出其分裂后生成的两个子 region(meta 表中 splitA 列和 splitB 列),只需要检查此两个子 region 是否还存在引用文件,如果都不存在引用文件就可以认为该父 region 对应的文件可以被删除。现在再来看看上文中父目录在 meta 表中的信息,就大概可以理解为什么会存储这些信息了:
4. split 模块在生产线的一些坑?
有些时候会有同学反馈说集群中部分 region 处于长时间 RIT,region 状态为 spliting。通常情况下都会建议使用 hbck 看下什么报错,然后再根据 hbck 提供的一些工具进行修复,hbck 提供了部分命令对处于 split 状态的 rit region 进行修复,主要的命令如下:
其中最常见的问题是 :
简单解释一下,这个错误是说 reference 文件所引用的父 region 文件不存在了,如果查看日志的话有可能看到如下异常:
父 region 文件为什么会莫名其妙不存在?经过和朋友的讨论,确认有可能是因为官方 bug 导致,详见 HBASE-13331。这个 jira 是说 HMaster 在确认父目录是否可以被删除时,如果检查引用文件(检查是否存在、检查是否可以正常打开)抛出 IOException 异常,函数就会返回没有引用文件,导致父 region 被删掉。正常情况下应该保险起见返回存在引用文件,保留父 region,并打印日志手工介入查看。如果大家也遇到类似的问题,可以看看这个问题,也可以将修复 patch 打到线上版本或者升级版本。
End.
来源: http://www.36dsj.com/archives/104841