一、 业务背景
业务优化需要将海量的回调数据处理后存入hbase表,供后续查询,分析,导出等处理,hbase在存储大量数据时,首先要考虑的是热点与预分区的问题,本文主要探讨热点产生的原因、如何结合实际业务进行合理的预分区。
二、 热点产生原因
想知道hbase数据热点产生的原因,需要先了解hbase的存储结构,入下图所示
HBase中的每张表都通过行键按照一定的范围被分割成多个子表(HRegion),默认一个HRegion超过256M就要被分割成两个,由HRegionServer管理,管理哪些HRegion由HMaster分配,通常较少的region数量可使群集运行的更加平稳,官方指出每个RegionServer大约100个regions。
HRegionServer存取一个子表时,会创建一个HRegion对象,然后对表的每个列族(Column Family)创建一个Store实例,每个Store都会有0个或多个StoreFile与之对应,每个StoreFile都会对应一个HFile, HFile就是实际的存储文件。因此,一个HRegion有多少个列族就有多少个Store。
另外,每个HRegion还拥有一个MemStore实例。
也就是说我们的HBase的表会被划分为1个或多个Region,被托管在RegionServer中。
可以看到上面的表有两个region,同时我们可以看到region有两个重要的属性:StartKey和EndKey。表示这个Region维护的rowkey的范围,当我们要读写数据时,如果rowkey落在某个start-end key范围内,那么就会定位到目标region并且读写到相关的数据。
默认情况下,当我们通过hbaseAdmin来创建一张表时,刚开始的时候只有一个Region,start-endkey没有边界,所有进入的数据都会被接收并存储。
所有的rowkey都会写入当前的region,随着数据量的增长,region的size变大,当到达阀值时,hbase会将region一分为二成两个region,这个过程称为region-split,源码在以下位置
org/apache/hadoop/hbase/regionserver/SplitRequest.java
private void requestRegionSplit() {
final TableName table = parent.getTable();
final RegionInfo hri_a = RegionInfoBuilder.newBuilder(table)
.setStartKey(parent.getStartKey())
.setEndKey(midKey)
.build();
final RegionInfo hri_b = RegionInfoBuilder.newBuilder(table)
.setStartKey(midKey)
.setEndKey(parent.getEndKey())
.build();
// Send the split request to the master. the master will do the validation on the split-key.
// The parent region will be unassigned and the two new regions will be assigned.
// hri_a and hri_b objects may not reflect the regions that will be created, those objects
// are created just to pass the information to the reportRegionStateTransition().
if (!server.reportRegionStateTransition(new RegionStateTransitionContext(
TransitionCode.READY_TO_SPLIT, HConstants.NO_SEQNUM, -1, parent, hri_a, hri_b))) {
LOG.error("Unable to ask master to split " + parent.getRegionNameAsString());
}
}
如果我们在创建hbase表的时候,不进行预分区设置,默认只有一个region,一般情况我们的rowkey是顺序增长的,这样会存在问题:我们总是向最大的startkey所在的region写数据,因为我们的rowkey总是会比之前的大,并且hbase的是按升序方式排序的。所以写操作总是被定位到无上界的那个region中,之前分裂出来的region不会被写数据,所以这样产生的结果是不利的。
同时如果写请求很频繁,数据量增长很快,split的次数就会变多,每次分裂都伴随着资源消耗,所以我们不希望这种情况经常发生,所以我们可以采用rowkey做散列、预分区的方式来解决问题。
采用预分区方式创建hbase表,就是提前创建好多个region,所有region都维护自己的start-key和end-key,我们知道在hbase中,表的所有行都是按照rowkey 的字典序排列的,表在行的方向上分割为多个region,那么当我们的rowkey包含业务逻辑,比如号码,那么创建预分区后,如果直接拿号码做rowkey,那么按字典排列,就会造成大量数据进入同一个region,达到阈值后,region分裂,这样既形成了热点数据,也导致预分区失去了作用,所以当我们采用预分区时,要考虑如何设计rowkey来避免热点数据的出现。
我们总结一下:热点问题主要原因在于rowkey的设计不合理.在某个时间段,对HBase的读写请求集中到少数几个region上面,导致这些region所属的regionserver请求量比较大,负载压力增加,而其他regionserver属于空闲状态,一般这种问题就是hbase的rowkey热点问题了
三、 如何避免热点数据出现
针对业务中,rowkey是由号码组成的,所以下述我们针对号码来讨论如何解决热点问题
加盐
我们知道由于hbase行都是按照rowkey 的字典序排列的,预分区后region的startKey和endKey的范围,就是rowKey的前缀,也就是说rowKey的前缀决定了数据会进入哪个region.那么我们在rowKey前面加入随机数,就可以保证数据可以分散的进入不同的region了。首先我们通过javaApi创建预分区表
public static void createTable(Connection connection, String tName, String... columnFamily){
try(Admin admin =connection.getAdmin()){
TableName tableName = TableName.valueOf(tName);
//判断表是否存在
if(admin.tableExists(tableName)){
logger.error("表" + tName + "已存在");
//System.exit(0);
}else{
//创建表属性对象,表名需要转字节
HTableDescriptor descriptor = new HTableDescriptor(tableName);
//创建多个列族
for(String cf : columnFamily){
descriptor.addFamily(new HColumnDescriptor(cf));
}
//创建256个region,001-255,三位数字分区,key的头也为三位数字
byte[][] regions = new byte[255][];
for (int i = 1; i < 256; i++) {
String mk = String.valueOf(i);
if(mk.length() == 1){
mk = "00" + mk;
} else if(mk.length() == 2){
mk = "0" + mk;
}
regions[i - 1] = mk.getBytes();
}
//根据对表的配置,创建表
admin.createTable(descriptor,regions);
logger.info("表" + tableName + "创建成功!");
}
} catch (Exception e){
logger.error("error", e);
}
}
上述代码中我们按001-255区间范围 分了256个预分区,可以结合实际业务进行region数的调整
rowKey在号码前面拼接随机数,通过测试发现数据可以均匀进入region,但是实际业务中出现问题,每个号码可能发送多条数据,hbase中要做更新操作,如果rowKey包含随机部分,就无法实现更新操作,所以该方案不适合我们,那么我们就需要一种能固定hash并且不会形成热点的前缀算法。经过寻找,我们直接使用redis的一种实现算法,如下
/**
* 通过号码获取均匀散列映射到256范围内
* @param phone
* @return
*/
public static String getTableIdByPhone(String phone) {
int hash = CRC16.getCRC16(phone) % SIZE;
String mk = String.valueOf(hash);
if(mk.length() == 1){
mk = "00" + mk;
} else if(mk.length() == 2){
mk = "0" + mk;
}
return mk;
}
SIZE是我们的预分区数量,通过该算法可以将号码均匀散列到指定范围内。将随机数替换为散列生成算法,就可以解决热点问题了。
四、rowkey设计原则
我们总结一下hbase 的rowkey设计原则
rowkey的长度尽量短
散列原则,将数据分散到不同的region中
表设计要考虑好热点问题
保证rowkey是唯一的
下期我们会总结一下hbase的使用与优化。