HBase 学习:Region 迁移、合并、分裂和负载均衡

本文最后更新于:2021年6月10日 下午

负载均衡是分布式系统的必备功能,多个节点组成的分布式系统必须通过负载均衡机制保证各个节点之间负载的均衡性,一旦出现负载非常集中的情况,就很有可能导致对应的部分节点响应变慢,进而拖慢甚至拖垮整个集群。HBase 基于 Region 的数量实现负载均衡。平衡 Region 数量会进行 Region 迁移。为了避免单个 Region 过大或者过小,可以对 Region 做合并或分裂操作。本文首先介绍 Region 的迁移、合并和分裂,最后介绍基于 Region 的负载均衡策略。

Region 迁移

作为一个分布式系统,分片迁移是最基础的核心功能。集群负载均衡、故障恢复等功能都是建立在分片迁移的基础之上的。比如集群负载均衡,可以简单理解为集群中所有节点上的分片数目保持相同。实际执行分片迁移时可以分为两个步骤:第一步,根据负载均衡策略制定分片迁移计划;第二步,根据迁移计划执行分片的实际迁移。

HBase 系统中,分片迁移就是 Region 迁移。和其他很多分布式系统不同,HBase 中 Region 迁移是一个非常轻量级的操作。所谓轻量级,是因为 HBase 的数据实际存储在 HDFS 上,不需要独立进行管理,因而 Region 在迁移的过程中不需要迁移实际数据,只要将读写服务迁移即可。

迁移过程

Region 迁移大体上分为两个阶段:unassign 阶段和 assign 阶段。

unassign region 的具体流程为:

  • create zk closing node。该节点在/unassigned 路径下, 包含(znode 状态,region 名字,原始 RS 名,payload)这些数据。
  • hmaster 调用 rpc 服务关闭 region server。region-close 的流程大致为先获取 region 的 writeLock , 然后 flush memstore, 再并发关闭该 region 下的所有的 store file 文件(注意一个 region 有多个 store,每个 store 又有多个 store file , 所以可以实现并发 close store file) 。最后释放 region 的 writeLock。
  • 设置 zk closing node 的 znode 状态为 closed。

assgin region 的具体流程为:

  • 获取到对应的 Region Plan。
  • HMaster 调用 rpc 服务去 Region Plan 对应的 RegionServer 上 open region。这里会先更新/unassigned 节点为 opening。然后并发 Load HStore,再更行 zk/ROOT/META 表信息,这里是为了 client 下次能获取到正确的路由信息, 最后更新 region 状态为 OPEN。

Region In Transition (RIT)

Region-In-Transition 说的是 Region 变迁机制,实际上是指在一次特定操作行为中 Region 状态的变迁。HBase 定义的 Region 状态见下表:

状态 说明
OFFLINE 下线状态
OPENING region 正在打开
OPEN region 正常打开
FAILED_OPEN region 打开失败
CLOSING region 正在关闭
CLOSED region 正常关闭
FAILED_CLOSE region 关闭失败
SPLITTING region 正在执行分裂
SPLIT region 完成分裂
SPLITTING_NEW 分裂过程中产生新 region
MERGING region 正在执行合并
MERGED region 合并完成
MERGING_NEW 两个 region 合并过程后形成新 region

region 迁移过程中会伴随 region 状态的不断变迁。因为 unassign/assign 操作都是由多个子操作组成的,涉及到 RS、Master、ZK 多个组件之间的协调合作,只有记录 region 状态才能知道当前 unassign/assign 的进度,在异常发生时才能根据具体进度继续执行。

region 的这些状态会存储在三个区域: meta 表、master 内存和 zk 的 region-in-transition 节点。它们的作用分别如下:

  • meta 表只存储 region 所在 rs,并不存储迁移过程中的中间状态。
  • master 内存中存储了整个集群所有的 region 信息,可以获取任意一个 region 当前的状态以及存储在哪个 rs 上。
    master 存储的 region 状态变更都是由 rs 通过 zk 通知给 master 的,因此 master 存储的 region 状态变更总是滞后于真正的 region 状态变更。
  • zk 中存储的是临时性的状态转移信息,作为 master 和 rs 之间反馈 region 状态的通道。

只有 rs、master 和 zk 中 region 的状态都保持一致时,对应的 region 才处于正常的工作状态。当 region 状态在三个地方不能保持一致时,就会出现 region-in-transition(RIT)现象。

Assignment Manager V2

通过上面的分析发现,虽然迁移是一个轻量级操作,但是实现逻辑非常复杂。存在如下缺点:

  • region 状态变化复杂
  • region 状态多处缓存
  • 重度依赖 Zookeeper

当 region 数达到百万级别时,Assignment Mananger 成为了一个严重瓶颈。目前 hbase2.0 已结有了新的实现方案(Assignment ManangerV2),完全摆脱了 zk 依赖。它引入了 ProcedureV2 这个持久化存储来保存 Region transition 中的各个状态,保证在 master 重启时,之前的 assing/unassign,split 等任务能够从中断点重新执行。再者,之前的 AM 使用的 Zookeeper watch 机制通知 master region 状态的改变,而现在每当 RegionServer Open 或者 close 一个 region 后,都会直接发送 RPC 给 master 汇报,因此也不需要 Zookeeper 来做状态的通知。

Region 合并

相比 Region 分裂,在线合并 Region 的使用场景比较有限,最典型的一个应用场景是,在某些业务中本来接收写入的 Region 在之后的很长时间都不再接收任何写入,而且 Region 上的数据因为 TTL 过期被删除。这种场景下的 Region 实际上没有任何存在的意义,称为空闲 Region。一旦集群中空闲 Region 很多,就会导致集群管理运维成本增加。此时,可以使用在线合并功能将这些 Region 与相邻的 Region 合并,减少集群中空闲 Region 的个数。

Region 合并的主要流程如下:

  1. 客户端发送 merge 请求给 Master。
  2. Master 将待合并的所有 Region 都 move 到同一个 RegionServer 上。
  3. Master 发送 merge 请求给该 RegionServer。
  4. RegionServer 启动一个本地事务执行 merge 操作。
  5. merge 操作将待合并的两个 Region 下线,并将两个 Region 的文件进行合并。
  6. 将这两个 Region 从 hbase:meta 中删除,并将新生成的 Region 添加到 hbase:meta 中。
  7. 将新生成的 Region 上线。

HBase 使用 merge_region 命令执行 Region 合并。merge_region 是一个异步操作,命令执行之后会立刻返回,用户需要一段时间之后手动检测合并是否成功。默认情况下 merge_region 命令只能合并相邻的两个 Region,非相邻的 Region 无法执行合并操作。同时 HBase 也提供了一个可选参数 true,使用此参数可以强制让不相邻的 Region 进行合并,因为该参数风险较大,一般并不建议生产线上使用。

Region 分裂

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

Region分裂触发策略

在当前版本中,HBase 已经有 6 种分裂触发策略。每种触发策略都有各自的适用场景,用户可以根据业务在表级别选择不同的分裂触发策略。

image-20201113165629125

  • 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 中。

寻找SplitPoint

region 切分策略会触发 region 切分,切分开始之后的第一件事是寻找切分点 splitpoint。切分点整个 region 中最大 store 中的最大文件中最中心的一个 block 的首个 rowkey。另外,HBase 还规定,如果定位到的 rowkey 是整个文件的首个 rowkey 或者最后一个 rowkey 的话,就认为没有切分点。没有切分点最常见的就是一个文件只有一个 block,执行 split 的时候就会发现无法切分。

Region切分核心流程

HBase 将整个切分过程包装成了一个事务,意图能够保证切分事务的原子性。整个分裂事务过程分为三个阶段:prepare – execute – (rollback) 。

  • prepare 阶段:在内存中初始化两个子 region,具体是生成两个 HRegionInfo 对象,包含 tableName、regionName、startkey、endkey 等。同时会生成一个 transaction journal,这个对象用来记录切分的进展,具体见 rollback 阶段。

  • execute 阶段:

    • RegionServer 修改 /hbase/region-in-transition/region-name 节点信息为 SPLITING。

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

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

    • 关闭 parent region:parent region 关闭数据写入并触发 flush 操作,将写入 region 的数据全部持久化到磁盘。此后短时间内客户端落在父 region 上的请求都会抛出异常 NotServingRegionException。客户端会间隔一定时间后重试几次,正在关闭的 region 会被刷新。

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

    • 父 region 分裂为两个子 region 后,将 daughter A、daughter B 拷贝到 HBase 根目录下,形成两个新的 region。

    • parent region 通知修改.META.表后下线,不再提供服务。下线后 parent region 在 meta 表中的信息并不会马上删除,而是标注 split 列、offline 列为 true,并记录两个子 region。

      image-20201113183922514

    • 开启 daughter A、daughter B 两个子 region。通知修改 hbase.meta 表,正式对外提供服务。

      image-20201113183955342

    • RegionServer 更新/hbase/region-in-transition/region-name 节点信息为SPLIT, Master 会知道状态改变,如果有需要,Balancer 会重新分派子 region 到其他 RegionServer。

    • 在分裂后,.META. 和 HDFS 有 reference file 指向父 region,这些 references 在 region 被压缩重写数据文件时被清除。在 Master 中的 Garbage collection 任务将周期性的检查子 region 是否仍然引用父 region,如果没有引用,父 region 将被清除。

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

    image-20201113175426581

Region切分事务性保证

整个 region 切分是一个比较复杂的过程,涉及到父 region 中 HFile 文件的切分、两个子 region 的生成、系统 meta 元数据的更改等很多子步骤,因此必须保证整个切分过程的事务性,即要么切分完全成功,要么切分完全未开始,在任何情况下也不能出现切分只完成一半的情况。

为了实现事务性,hbase 设计了使用状态机(见 SplitTransaction 类)的方式保存切分过程中的每个子步骤状态,这样一旦出现异常,系统可以根据当前所处的状态决定是否回滚,以及如何回滚。

遗憾的是,目前实现中这些中间状态都只存储在内存中,因此一旦在切分过程中出现 regionserver 宕机的情况,有可能会出现切分处于中间状态的情况,也就是 RIT 状态。这种情况下需要使用 hbck 工具进行具体查看并分析解决方案。

在 2.0 版本之后,HBase 实现了新的分布式事务框架 Procedure V2(HBASE-12439),新框架将会使用 HLog 存储这种单机事务(DDL 操作、Split 操作、Move 操作等)的中间状态,因此可以保证即使在事务执行过程中参与者发生了宕机,依然可以使用 HLog 作为协调者对事务进行回滚操作或者重试提交,大大减少甚至杜绝 RIT 现象。

Region切分后续

通过 region 切分流程的了解,我们知道整个 region 切分过程并没有涉及数据的移动,所以切分成本本身并不是很高,可以很快完成。切分后子 region 的文件实际没有任何用户数据,文件中存储的仅是一些元数据信息-切分点 rowkey 等,那通过引用文件如何查找数据呢?子 region 的数据实际在什么时候完成真正迁移?数据迁移完成之后父 region 什么时候会被删掉?

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

整个流程如下图所示:

image-20201113185600836

  1. 根据 reference 文件名(region 名+真实文件名)定位到真实数据所在文件路径。
  2. 定位到真实数据文件就可以在整个文件中扫描待查 KV 了么?非也。因为 reference 文件通常都只引用了数据文件的一半数据,以切分点为界,要么上半部分文件数据,要么下半部分数据。那到底哪部分数据?切分点又是哪个点?还记得上文又提到 reference 文件的文件内容吧,没错,就记录在文件中。
父region的数据什么时候会迁移到子region目录?

子 region 执行 Major Compaction 后会将父目录中属于该子 region 的所有数据读出来并写入子 region 目录数据文件中。

父region什么时候会被删除?

实 HMaster 会启动一个线程定期遍历检查所有处于 splitting 状态的父 region,确定检查父 region 是否可以被清理。检测线程首先会在 meta 表中揪出所有 split 列为 true 的 region,并加载出其分裂后生成的两个子 region(meta 表中 splitA 列和 splitB 列),只需要检查此两个子 region 是否还存在引用文件,如果都不存在引用文件就可以认为该父 region 对应的文件可以被删除。

image-20201113185629824

负载均衡

Rebalance 策略

HBase 官方目前支持两种负载均衡策略:SimpleLoadBalancer 策略和 StochasticLoadBalancer 策略。

此外,HDFS-6133 通过在 HDFS 服务配置中将dfs.datanode.block-pinning.enabled属性设置为true,可以从 HDFS 负载均衡器中排除优先节点(固定)块。通过将 HBase 平衡器类(conf:hbase.master.loadbalancer.class)切换为org.apache.hadoop.hbase.favored.FavoredNodeLoadBalancer,可以启用 HBase 以使用 HDFS 优先节点功能。HDFS-6133 在 HDFS 2.7.0 及更高版本中可用,但 HBase 不支持在 HDFS 2.7.0 上运行,因此必须使用 HDFS 2.7.1 或更高版本才能将此功能与 HBase 一起使用。

SimpleLoadBalancer策略

这种策略能够保证每个 RegionServer 的 Region 个数基本相等,假设集群中一共有n个 RegionServer,m个 Region,那么集群的平均负载就是average=m/n,这种策略能够保证所有 RegionServer 上的 Region 个数都在[floor(average),ceil(average)]之间。
因此,SimpleLoadBalancer 策略中负载就是 Region 个数,集群负载迁移计划就是 Region 从个数较多的 RegionServer 上迁移到个数较少的 RegionServer 上。

这种策略没有考虑 RegionServer 上的读写 QPS、数据量大小等因素。这样就可能出现一种情况:虽然集群中每个 RegionServer 的 Region 个数都基本相同,但因为某台 RegionServer 上的 Region 全部都是热点数据,导致 90%的读写请求还是落在了这台 RegionServer 上,这样显而易见没有达到负载均衡的目的。

StochasticLoadBalancer策略

StochasticLoadBalancer 是 HBase 的默认策略,简单来讲,是一种综合权衡一下 6 个因素的均衡策略:

  • 每台 RegionServer 读请求数(ReadRequestCostFunction)
  • 每台 RegionServer 写请求数(WriteRequestCostFunction)
  • 每台 RegionServer 的 Region 个数(RegionCountSkewCostFunction)
  • 移动代价(MoveCostFunction)
  • 数据 locality(TableSkewCostFunction)
  • 每张表占据 RegionServer 中 region 个数上限(LocalityCostFunction)

对于 cluster 的每一种 region 分布, 采用 6 个因素加权的方式算出一个代价值,这个代价值就用来评估当前 region 分布是否均衡,越均衡则代价值越低。然后通过成千上万次随机迭代来找到一组 RegionMove 的序列,使得最终的代价值严格递减。 得到的这一组 RegionMove 就是 HMaster 最终执行的 region 迁移方案。

生成 RegionMove 的策略有以下三种:

  1. 随机选择两个 RS, 从每个 RS 中随机选择两个 Region,然后生成一个 Action, 这个 Action 有一半概率做 RegionMove(从 Region 多的 RS 迁移到 Region 少的 RS), 另一半概率做 RegionSwap(两个 RS 之间做 Region 的交换)。
  2. 选择 Region 最多的 RS 和 Region 最少的 RS,然后生成一个 Action, 这个 Action 一半概率做 RegionMove, 一半概率做 RegionSwap。
  3. 随机找一个 RS,然后找到该 RS 上数据 locality 最差的 Region,再找到 Region 大部分数据落在的 RS,然后生成一个 Action,该 Action 用来把 Region 迁移到它应该所在的 RS,用来提高 locality。

Rebalance 触发

HMaster 会有个 BalancerChore 定时类去检查触发,间隔时间:hbase.balancer.period (默认值:300000s=5min) 。当然 HBaseAdmin 或 shell 也提供了命令接口可以手动触发。

Rebalance 方式

有两种方式,默认是把当前 RS 的 region 混在一起去 rebalance,若hbase.master.loadbalance.bytable=true,则会按照一个表一个表来 rebalance,这样至少可以确保某些表中途是 rebalance 完的。有人会称后者为表级 rebalance,但实际上这并不是纯粹的表级 rebalance,如果你真的只想触发某个表的 rebalance,可能得考虑自定义策略去过滤其他表的 region 并且将上述 bytable 配置配置为 true。

参考资料

HBase原理与实践

HBase运维实践-聊聊RIT的那点事

HBase – Region分裂

HBase rebalance 负载均衡源码角度解读使用姿势

HBase Region Balance实践


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!