HBASE Region数量增多问题描述及解决方案

2021-06-16 17:22:26 浏览数 (1)

文章目录

    • HBASE Region数量增多问题描述及解决方案
      • 1. 问题描述
        • 1.1 HBase 分区概念
        • 1.2 region过多影响
        • 1.3 合理region数量
      • 2. 方案概述
        • 2.1 ROWKEY 修改方案细节
          • 2.1.1 Rowkey长度原则(最好不超过16字节)
          • 2.1.2 rowkey散列原则
          • 2.1.3 rowkey唯一原则
          • 2.1.4 时间戳反转
          • 2.1.5 整个rowkey如下(timestamp并不是必要的,视业务而定)
        • 2.2 自动合并region方案
          • 2.2.1 如何进行Region合并
          • 2.2.2 批量合并
          • 2.2.3 问题
        • 2.3 参数调整
      • Ref

HBASE Region数量增多问题描述及解决方案

1. 问题描述

1.1 HBase 分区概念

HBase每张表在底层存储上是由至少一个Region组成,Region实际上就是HBase表的分区。HBase新建一张表时默认Region即分区的数量为1,随着数据增长一个分区在达到一定大小时会自动Split,一分为二。

通常情况下,生产环境的每个regionserver节点上会有很多Region存在,我们一般比较关心每个节点上的Region数量,主要为了防止HBase分区过多影响到集群的稳定性。

1.2 region过多影响

分区过多会带来很多不好的影响,主要体现在以下几个方面。

  1. 频繁刷写 我们知道Region的一个列族对应一个MemStore,假设HBase表都有统一的1个列族配置,则每个Region只包含一个MemStore。通常HBase的一个MemStore默认大小为128 MB,见参数hbase.hregion.memstore.flush.size。当可用内存足够时,每个MemStore可以分配128 MB空间。当可用内存紧张时,假设每个Region写入压力相同,则理论上每个MemStore会平均分配可用内存空间。 因此,当节点Region过多时,每个MemStore分到的内存空间就会很小。这个时候,写入很小的数据量就会被强制Flush到磁盘,将会导致频繁刷写。频繁刷写磁盘,会对集群HBase与HDFS造成很大的压力,可能会导致不可预期的严重后果。
  2. 压缩风暴 因Region过多导致的频繁刷写,将在磁盘上产生非常多的HFile小文件,当小文件过多的时候HBase为了优化查询性能就会做Compaction操作,合并HFile减少文件数量。当小文件一直很多的时候,就会出现 “压缩风暴”。Compaction非常消耗系统io资源,还会降低数据写入的速度,严重的会影响正常业务的进行。
  3. MSLAB内存消耗较大 MSLAB(MemStore-local allocation buffer)存在于每个MemStore中,主要是为了解决HBase内存碎片问题,默认会分配 2 MB 的空间用于缓存最新数据。如果Region数量过多,MSLAB总的空间占用就会比较大。比如当前节点有1000个包含1个列族的Region,MSLAB就会使用1.95GB的堆内存,即使没有数据写入也会消耗这么多内存。
  4. Master assign region时间较长 HBase Region过多时Master分配Region的时间将会很长。特别体现在重启HBase时Region上线时间较长,严重的会达到小时级,造成业务长时间等待的后果。
  5. 影响MapReduce并发数 当使用MapReduce操作HBase时,通常Region数量就是MapReduce的任务数,Region数量过多会导致并发数过多,产生过多的任务。任务太多将会占用大量资源,当操作包含很多Region的大表时,占用过多资源会影响其他任务的执行。
1.3 合理region数量

关于每个regionserver节点分区数量大致合理的范围,HBase官网上也给出了定义:

Generally less regions makes for a smoother running cluster (you can always manually split the big regions later (if necessary) to spread the data, or request load, over the cluster); 20-200 regions per RS is a reasonable range.

可见,通常情况下每个节点拥有20200个Region是比较正常的。借鉴于20200这个区间范围,我们接下来具体讨论。

实际上,每个RegionServer的最大Region数量由总的MemStore内存大小决定。我们知道每个Region的每个列族对应一个MemStore,假设HBase表都有统一的1个列族配置,那么每个Region只包含一个MemStore。一个MemStore大小通常在128~256 MB,见参数hbase.hregion.memstore.flush.size。默认情况下,RegionServer会将自身堆内存的40%(见参数hbase.regionserver.global.memstore.size)供给节点上所有MemStore使用,如果所有MemStore的总大小达到该配置大小,新的更新将会被阻塞并且会强制刷写磁盘。因此,每个节点最理想的Region数量应该由以下公式计算(假设HBase表都有统一的列族配置):

((RS memory) * (total memstore fraction)) / ((memstore size)*(column families))

其中:

  • RS memory:表示regionserver堆内存大小,即HBASE_HEAPSIZE。
  • total memstore fraction:表示所有MemStore占HBASE_HEAPSIZE的比例,HBase0.98版本以后由hbase.regionserver.global.memstore.size参数控制,老版本由hbase.regionserver.global.memstore.upperLimit参数控制,默认值0.4。
  • memstore size:即每个MemStore的大小,原生HBase中默认128M。
  • column families:即表的列族数量,通常情况下只设置1个,最多不超过3个。

举个例子,假如一个集群中每个RegionServer的堆内存是32GB,那么节点上最理想的Region数量应该是32768*0.4/128 ≈ 102,所以,当前环境中单节点理想情况下大概有102个Region。

这种最理想情况是假设每个Region上的填充率都一样,包括数据写入的频次、写入数据的大小,但实际上每个Region的负载各不相同,可能有的Region特别活跃负载特别高,有的Region则比较空闲。所以,通常我们认为2~3倍的理想Region数量也是比较合理的,针对上面举例来说,大概300个Region左右算是合理的。

如果实际的Region数量比2~3倍的计算值还要多,就要实际观察Region的刷写、压缩情况了,Region越多则风险越大。经验告诉我们,如果单节点Region数量过千,集群可能存在较大风险。

2. 方案概述

对于hbase region数量增多 负载高问题,大概有几种解决方案:

  1. 修改rowkey,使用时间戳倒序 ID的方案; pros: 根本解决问题 cons: 需要推动其他业务使用方修改
  2. 自动region合并脚本方案; pros: 不需要修改业务代码 cons: 需要定期执行,且有风险
  3. 修改hbase.hregion.memstore.flush.size等参数,使rs flush不这么频繁,负载小一点; pros: 不影响业务使用方 cons: 后期region数量增多还是不能根本解决问题
2.1 ROWKEY 修改方案细节

可以参考之前的博客

2.1.1 Rowkey长度原则(最好不超过16字节)

Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节。 原因如下:

  1. 数据的持久化文件HFile中是按照KeyValue存储的,如果Rowkey过长比如100个字节,1000万列数据光Rowkey就要占用100*1000万=10亿个字节,将近1G数据,这会极大影响HFile的存储效率;
  2. MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此Rowkey的字节长度越短越好。
  3. 目前操作系统是都是64位系统,内存8字节对齐。控制在16个字节,8字节的整数倍利用操作系统的最佳特性。
2.1.2 rowkey散列原则

把主键哈希后当成rowkey的头部

2.1.3 rowkey唯一原则

必须在设计上保证其唯一性,rowkey是按照字典顺序排序存储的,因此,设计rowkey的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。

2.1.4 时间戳反转

如果数据需要保留多个版本,可以使用反转的时间戳作为rowkey的一部分,用 Long.Max_Value - timestamp 追加到key的末尾,例如 [key][reverse_timestamp] , [key] 的最新值可以通过scan [key]获得[key]的第一条记录,因为HBase中rowkey是有序的,第一条记录是最后录入的数据。

2.1.5 整个rowkey如下(timestamp并不是必要的,视业务而定)
代码语言:javascript复制
rowkey=哈希(主键<递增的id手机号码等>) Long.Max_Value - timestamp
2.2 自动合并region方案
2.2.1 如何进行Region合并

HBase有提供一个合并Region的命令,具体操作如下:

代码语言:javascript复制
# 合并相邻的两个Region
hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME'
# 强制合并两个Region
hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME', true

但是,这种方式会有一个问题,就是只能一次合并2个Region,如果这里有几千个Region需要合并,这种方式是不可取的。

2.2.2 批量合并

这里有一种批量合并的方式,就是通过编写脚本(merge_small_regions.rb)来实现,实现代码如下:

代码语言:javascript复制
# Test Mode:
#
# hbase org.jruby.Main merge_empty_regions.rb namespace.tablename   
#
# Non Test - ie actually do the merge:
#
# hbase org.jruby.Main merge_empty_regions.rb namespace.tablename   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)如下:

代码语言:javascript复制
#! /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操作,具体操作命令如下:

代码语言:javascript复制
# 默认循环10次,例如本次循环执行5次
sh merging-region.sh 5
2.2.3 问题

在合并Region的过程中出现永久RIT(Region-In-Transition)怎么办?在批量合并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流程中了。

可以参考 http://hbasefly.com/2016/09/08/hbase-rit/

2.3 参数调整

region server gc较为频繁的问题,可对hbase相关参数进行优化:

  1. 将hbase master内存参数HBase Master Maximum Java heap size由2048调整到12288。
  2. hbase租约优化 hbase.regionserver.lease.period = 240000; hbase.rpc.timeout=280000;
  3. RegionServers maximum value for -Xmn由512调整到2014;
  4. 设置HBase Region Major Compaction参数从604800000(7天)改为0,关闭自动major compaction,后期通过执行命令进行手动MAJOR compaction操作;
  5. 设置hbase.hstore.compaction.min参数为10,参数代表着一次minor compaction最少合并的HFile数量,默认值 3。表示至少有3个符合条件的HFile,minor compaction才会启动。并确认hbase.hstore.compaction.max的值大于10;
  6. HBase HStore compaction threshold 从3变更为5,参数代表着一个store里面允许存在的hfile个数,超过后会合并。调大该参数;
  7. 修改hbase.hregion.memstore.flush.size参数,使rs flush不这么频繁

对Hbase变更后 需要重启hbase (该操作有风险)

Ref

  1. http://hbasefly.com/2016/09/08/hbase-rit/
  2. https://cloud.tencent.com/developer/article/1488479

0 人点赞