1. 概述
HBase 中表的基本单位是 Region, 日常在调用 HBase API 操作一个表时, 交互的数据也会以 Region 的形式进行呈现. 一个表可以有若干个 Region, 今天笔者就来和大家分享一下 Region 合并的一些问题和解决方法.
2. 内容
在分析合并 Region 之前, 我们先来了解一下 Region 的体系结构, 如下图所示:
从图中可知, 能够总结以下知识点:
HRegion: 一个 Region 可以包含多个 Store;
Store: 每个 Store 包含一个 Memstore 和若干个 StoreFile;
StoreFile: 表数据真实存储的地方, HFile 是表数据在 HDFS 上的文件格式.
如果要查看 HFile 文件, HBase 有提供命令, 命令如下:
hbase hfile -p -f /hbase/data/default/ip_login/d0d7d881bb802592c09d305e47ae70a5/_d/7ec738167e9f4d4386316e5e702c8d3d
执行输出结果, 如下图所示:
2.1 为什么需要合并 Region
那为什么需要合并 Region 呢? 这个需要从 Region 的 Split 来说. 当一个 Region 被不断的写数据, 达到 Region 的 Split 的阀值时(由属性 hbase.hregion.max.filesize 来决定, 默认是 10GB), 该 Region 就会被 Split 成 2 个新的 Region. 随着业务数据量的不断增加, Region 不断的执行 Split, 那么 Region 的个数也会越来越多.
一个业务表的 Region 越多, 在进行读写操作时, 或是对该表执行 Compaction 操作时, 此时集群的压力是很大的. 这里笔者做过一个线上统计, 在一个业务表的 Region 个数达到 9000 + 时, 每次对该表进行 Compaction 操作时, 集群的负载便会加重. 而间接的也会影响应用程序的读写, 一个表的 Region 过大, 势必整个集群的 Region 个数也会增加, 负载均衡后, 每个 RegionServer 承担的 Region 个数也会增加.
因此, 这种情况是很有必要的进行 Region 合并的. 比如, 当前 Region 进行 Split 的阀值设置为 30GB, 那么我们可以对小于等于 10GB 的 Region 进行一次合并, 减少每个业务表的 Region, 从而降低整个集群的 Region, 减缓每个 RegionServer 上的 Region 压力.
2.2 如何进行 Region 合并
那么我们如何进行 Region 合并呢? HBase 有提供一个合并 Region 的命令, 具体操作如下:
- # 合并相邻的两个 Region
- hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME'
- # 强制合并两个 Region
- hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME', true
但是, 这种方式会有一个问题, 就是只能一次合并 2 个 Region, 如果这里有几千个 Region 需要合并, 这种方式是不可取的.
2.2.1 批量合并
这里有一种批量合并的方式, 就是通过编写脚本 (merge_small_regions.rb) 来实现, 实现代码如下:
- # Test Mode:
- #
- # hbase org.jruby.Main merge_empty_regions.rb namespace.tablename <skip_size> <batch_regions> <merge?>
- #
- # Non Test - ie actually do the merge:
- #
- # hbase org.jruby.Main merge_empty_regions.rb namespace.tablename <skip_size> <batch_regions> merge
- #
- # Note: Please replace namespace.tablename with your namespace and table, eg NS1.MyTable. This value is case sensitive.
- require 'digest'
- require 'java'
- java_import org.apache.hadoop.hbase.HBaseConfiguration
- java_import org.apache.hadoop.hbase.client.HBaseAdmin
- java_import org.apache.hadoop.hbase.TableName
- java_import org.apache.hadoop.hbase.HRegionInfo;
- java_import org.apache.hadoop.hbase.client.Connection
- java_import org.apache.hadoop.hbase.client.ConnectionFactory
- java_import org.apache.hadoop.hbase.client.Table
- java_import org.apache.hadoop.hbase.util.Bytes
- def list_bigger_regions(admin, table, low_size)
- cluster_status = admin.getClusterStatus()
- master = cluster_status.getMaster()
- biggers = []
- cluster_status.getServers.each do |s|
- cluster_status.getLoad(s).getRegionsLoad.each do |r|
- # getRegionsLoad returns an array of arrays, where each array
- # is 2 elements
- # Filter out any regions that don't match the requested
- # tablename
- next unless r[1].get_name_as_string =~ /#{table}\,/
- if r[1].getStorefileSizeMB()> low_size
- if r[1].get_name_as_string =~ /\.([^\.]+)\.$/
- biggers.push $1
- else
- raise "Failed to get the encoded name for #{r[1].get_name_as_string}"
- end
- end
- end
- end
- biggers
- end
- # Handle command line parameters
- table_name = ARGV[0]
- low_size = 1024
- if ARGV[1].to_i>= low_size
- low_size=ARGV[1].to_i
- end
- limit_batch = 1000
- if ARGV[2].to_i <= limit_batch
- limit_batch = ARGV[2].to_i
- end
- do_merge = false
- if ARGV[3] == 'merge'
- do_merge = true
- end
- config = HBaseConfiguration.create();
- connection = ConnectionFactory.createConnection(config);
- admin = HBaseAdmin.new(connection);
- bigger_regions = list_bigger_regions(admin, table_name, low_size)
- regions = admin.getTableRegions(Bytes.toBytes(table_name));
- puts "Total Table Regions: #{regions.length}"
- puts "Total bigger regions: #{bigger_regions.length}"
- filtered_regions = regions.reject do |r|
- bigger_regions.include?(r.get_encoded_name)
- end
- puts "Total regions to consider for Merge: #{filtered_regions.length}"
- filtered_regions_limit = filtered_regions
- if filtered_regions.length <2
- puts "There are not enough regions to merge"
- filtered_regions_limit = filtered_regions
- end
- if filtered_regions.length> limit_batch
- filtered_regions_limit = filtered_regions[0,limit_batch]
- puts "But we will merge : #{filtered_regions_limit.length} regions because limit in parameter!"
- end
- r1, r2 = nil
- filtered_regions_limit.each do |r|
- if r1.nil?
- r1 = r
- next
- end
- if r2.nil?
- r2 = r
- end
- # Skip any region that is a split region
- if r1.is_split()
- r1 = r2
- r2 = nil
- puts "Skip #{r1.get_encoded_name} bcause it in spliting!"
- next
- end
- if r2.is_split()
- r2 = nil
- puts "Skip #{r2.get_encoded_name} bcause it in spliting!"
- next
- end
- if HRegionInfo.are_adjacent(r1, r2)
- # only merge regions that are adjacent
- puts "#{r1.get_encoded_name} is adjacent to #{r2.get_encoded_name}"
- if do_merge
- admin.mergeRegions(r1.getEncodedNameAsBytes, r2.getEncodedNameAsBytes, false)
- puts "Successfully Merged #{r1.get_encoded_name} with #{r2.get_encoded_name}"
- sleep 2
- end
- r1, r2 = nil
- else
- puts "Regions are not adjacent, so drop the first one and with the #{r2.get_encoded_name} to iterate again"
- r1 = r2
- r2 = nil
- end
- end
- admin.close
该脚本默认是合并 1GB 以内的 Region, 个数为 1000 个. 如果我们要合并小于 10GB, 个数在 4000 以内, 脚本 (merging-region.sh) 如下:
- #! /bin/bash
- num=$1
- echo "[`date"+%Y-%m-%d %H:%M:%S"`] INFO : RegionServer Start Merging..."
- if [ ! -n "$num" ]; then
- echo "[`date"+%Y-%m-%d %H:%M:%S"`] INFO : Default Merging 10 Times."
- num=10
- elif [[ $num == *[!0-9]* ]]; then
- echo "[`date"+%Y-%m-%d %H:%M:%S"`] INFO : Input [$num] Times Must Be Number."
- exit 1
- else
- echo "[`date"+%Y-%m-%d %H:%M:%S"`] INFO : User-Defined Merging [$num] Times."
- fi
- for (( i=1; i<=$num; i++ ))
- do
- echo "[`date"+%Y-%m-%d %H:%M:%S"`] INFO : Merging [$i] Times,Total [$num] Times."
- hbase org.jruby.Main merge_small_regions.rb namespace.tablename 10240 4000 merge
- sleep 5
- done
在 merging-region.sh 脚本中, 做了参数控制, 可以循环来执行批量合并脚本. 可能在实际操作过程中, 批量执行一次 Region 合并, 合并后的结果 Region 还是有很多(可能此时又有新的 Region 生成), 这是我们可以使用 merging-region.sh 这个脚本多次执行批量合并 Region 操作, 具体操作命令如下:
- # 默认循环 10 次, 例如本次循环执行 5 次
- sh merging-region.sh 5
2.3 如果在合并 Region 的过程中出现永久 RIT 怎么办
在合并 Region 的过程中出现永久 RIT 怎么办? 笔者在生产环境中就遇到过这种情况, 在批量合并 Region 的过程中, 出现了永久 MERGING_NEW 的情况, 虽然这种情况不会影响现有集群的正常的服务能力, 但是如果集群有某个节点发生重启, 那么可能此时该 RegionServer 上的 Region 是没法均衡的. 因为在 RIT 状态时, HBase 是不会执行 Region 负载均衡的, 即使手动执行 balancer 命令也是无效的.
如果不解决这种 RIT 情况, 那么后续有 HBase 节点相继重启, 这样会导致整个集群的 Region 验证不均衡, 这是很致命的, 对集群的性能将会影响很大. 经过查询 HBase JIRA 单, 发现这种 MERGING_NEW 永久 RIT 的情况是触发了 https://issues.apache.org/jira/browse/HBASE-17682 的 BUG, 需要打上该 Patch 来修复这个 BUG, 其实就是 HBase 源代码在判断业务逻辑时, 没有对 MERGING_NEW 这种状态进行判断, 直接进入到 else 流程中了. 源代码如下:
- for (RegionState state : regionsInTransition.values()) {
- HRegionInfo hri = state.getRegion();
- if (assignedRegions.contains(hri)) {
- // Region is open on this region server, but in transition.
- // This region must be moving away from this server, or splitting/merging.
- // SSH will handle it, either skip assigning, or re-assign.
- LOG.info("Transitioning" + state + "will be handled by ServerCrashProcedure for" + sn);
- } else if (sn.equals(state.getServerName())) {
- // Region is in transition on this region server, and this
- // region is not open on this server. So the region must be
- // moving to this server from another one (i.e. opening or
- // pending open on this server, was open on another one.
- // Offline state is also kind of pending open if the region is in
- // transition. The region could be in failed_close state too if we have
- // tried several times to open it while this region server is not reachable)
- if (state.isPendingOpenOrOpening() || state.isFailedClose() || state.isOffline()) {
- LOG.info("Found region in" + state +
- "to be reassigned by ServerCrashProcedure for" + sn);
- rits.add(hri);
- } else if(state.isSplittingNew()) {
- regionsToCleanIfNoMetaEntry.add(state.getRegion());
- } else {
- LOG.warn("THIS SHOULD NOT HAPPEN: unexpected" + state);
- }
- }
- }
修复之后的代码如下:
- for (RegionState state : regionsInTransition.values()) {
- HRegionInfo hri = state.getRegion();
- if (assignedRegions.contains(hri)) {
- // Region is open on this region server, but in transition.
- // This region must be moving away from this server, or splitting/merging.
- // SSH will handle it, either skip assigning, or re-assign.
- LOG.info("Transitioning" + state + "will be handled by ServerCrashProcedure for" + sn);
- } else if (sn.equals(state.getServerName())) {
- // Region is in transition on this region server, and this
- // region is not open on this server. So the region must be
- // moving to this server from another one (i.e. opening or
- // pending open on this server, was open on another one.
- // Offline state is also kind of pending open if the region is in
- // transition. The region could be in failed_close state too if we have
- // tried several times to open it while this region server is not reachable)
- if (state.isPendingOpenOrOpening() || state.isFailedClose() || state.isOffline()) {
- LOG.info("Found region in" + state +
- "to be reassigned by ServerCrashProcedure for" + sn);
- rits.add(hri);
- } else if(state.isSplittingNew()) {
- regionsToCleanIfNoMetaEntry.add(state.getRegion());
- } else if (isOneOfStates(state, State.SPLITTING_NEW, State.MERGING_NEW)) {
- regionsToCleanIfNoMetaEntry.add(state.getRegion());
- }else {
- LOG.warn("THIS SHOULD NOT HAPPEN: unexpected" + state);
- }
- }
- }
但是, 这里有一个问题, 目前该 JIRA 单只是说了需要去修复 BUG, 打 Patch. 但是, 实际生产情况下, 面对这种 RIT 情况, 是不可能长时间停止集群, 影响应用程序读写的. 那么, 有没有临时的解决办法, 先临时解决当前的 MERGING_NEW 这种永久 RIT, 之后在进行 HBase 版本升级操作.
办法是有的, 在分析了 MERGE 合并的流程之后, 发现 HBase 在执行 Region 合并时, 会先生成一个初始状态的 MERGING_NEW. 整个 Region 合并流程如下:
从流程图中可以看到, MERGING_NEW 是一个初始化状态, 在 Master 的内存中, 而处于 Backup 状态的 Master 内存中是没有这个新 Region 的 MERGING_NEW 状态的, 那么可以通过对 HBase 的 Master 进行一个主备切换, 来临时消除这个永久 RIT 状态. 而 HBase 是一个高可用的集群, 进行主备切换时对用户应用来说是无感操作. 因此, 面对 MERGING_NEW 状态的永久 RIT 可以使用对 HBase 进行主备切换的方式来做一个临时处理方案. 之后, 我们在对 HBase 进行修复 BUG, 打 Patch 进行版本升级.
3. 总结
HBase 的 RIT 问题, 是一个比较常见的问题, 在遇到这种问题时, 可以先冷静的分析原因, 例如查看 Master 的日志, 仔细阅读 HBase web 页面 RIT 异常的描述, 使用 hbck 命令查看 Region, 使用 fsck 查看 HDFS 的 block 等. 分析出具体的原因后, 我们在对症下药, 做到大胆猜想, 小心求证.
4. 结束语
这篇博客就和大家分享到这里, 如果大家在研究学习的过程当中有什么问题, 可以加群进行讨论或发送邮件给我, 我会尽我所能为您解答, 与君共勉!
另外, 博主出书了《Hadoop 大数据挖掘从入门到进阶实战》, 喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习, 在此感谢大家的支持.
来源: https://www.cnblogs.com/smartloli/p/9649673.html