文章目录
- 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过多影响
分区过多会带来很多不好的影响,主要体现在以下几个方面。
- 频繁刷写
我们知道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造成很大的压力,可能会导致不可预期的严重后果。 - 压缩风暴 因Region过多导致的频繁刷写,将在磁盘上产生非常多的HFile小文件,当小文件过多的时候HBase为了优化查询性能就会做Compaction操作,合并HFile减少文件数量。当小文件一直很多的时候,就会出现 “压缩风暴”。Compaction非常消耗系统io资源,还会降低数据写入的速度,严重的会影响正常业务的进行。
- MSLAB内存消耗较大 MSLAB(MemStore-local allocation buffer)存在于每个MemStore中,主要是为了解决HBase内存碎片问题,默认会分配 2 MB 的空间用于缓存最新数据。如果Region数量过多,MSLAB总的空间占用就会比较大。比如当前节点有1000个包含1个列族的Region,MSLAB就会使用1.95GB的堆内存,即使没有数据写入也会消耗这么多内存。
- Master assign region时间较长 HBase Region过多时Master分配Region的时间将会很长。特别体现在重启HBase时Region上线时间较长,严重的会达到小时级,造成业务长时间等待的后果。
- 影响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数量增多 负载高问题,大概有几种解决方案:
- 修改rowkey,使用时间戳倒序 ID的方案; pros: 根本解决问题 cons: 需要推动其他业务使用方修改
- 自动region合并脚本方案; pros: 不需要修改业务代码 cons: 需要定期执行,且有风险
- 修改
hbase.hregion.memstore.flush.size
等参数,使rs flush不这么频繁,负载小一点; pros: 不影响业务使用方 cons: 后期region数量增多还是不能根本解决问题
2.1 ROWKEY 修改方案细节
可以参考之前的博客
2.1.1 Rowkey长度原则(最好不超过16字节)
Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节。 原因如下:
- 数据的持久化文件HFile中是按照KeyValue存储的,如果Rowkey过长比如100个字节,1000万列数据光Rowkey就要占用100*1000万=10亿个字节,将近1G数据,这会极大影响HFile的存储效率;
- MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此Rowkey的字节长度越短越好。
- 目前操作系统是都是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相关参数进行优化:
- 将hbase master内存参数HBase Master Maximum Java heap size由2048调整到12288。
- hbase租约优化
hbase.regionserver.lease.period
= 240000;hbase.rpc.timeout
=280000; - RegionServers maximum value for -Xmn由512调整到2014;
- 设置HBase Region Major Compaction参数从604800000(7天)改为0,关闭自动major compaction,后期通过执行命令进行手动MAJOR compaction操作;
- 设置
hbase.hstore.compaction.min
参数为10,参数代表着一次minor compaction最少合并的HFile数量,默认值 3。表示至少有3个符合条件的HFile,minor compaction才会启动。并确认hbase.hstore.compaction.max
的值大于10; - HBase HStore compaction threshold 从3变更为5,参数代表着一个store里面允许存在的hfile个数,超过后会合并。调大该参数;
- 修改
hbase.hregion.memstore.flush.size
参数,使rs flush不这么频繁
对Hbase变更后 需要重启hbase (该操作有风险)
Ref
- http://hbasefly.com/2016/09/08/hbase-rit/
- https://cloud.tencent.com/developer/article/1488479