zoukankan      html  css  js  c++  java
  • HBase Region合并分析

    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的情况是触发了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大数据挖掘从入门到进阶实战》,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。

  • 相关阅读:
    【洛谷P5305】旧词
    【洛谷P5470】序列
    【CF865D】Buy Low Sell High
    【洛谷P3242】接水果
    【洛谷P5048】Yuno loves sqrt technology III
    一、Java语言基础(1)_走进java——JDK-JRE-JVM概述
    一、Java语言基础(1)_走进java——跨平台/可移植性
    第一阶段:前端开发_JavaScript基础
    第一阶段:前端开发_HTML表单&CSS
    第一阶段:前端开发_HTML&CSS
  • 原文地址:https://www.cnblogs.com/smartloli/p/9649673.html
Copyright © 2011-2022 走看看