— hbase — 1 min read
该篇文章主要记录遇到hbase热点相关问题,以及RowKey设计和预分区相关总结。
第二个原因中所带来问题的解决办法就是设计一个合理的rowkey,尽可能的将所有的数据散列到不同的分区,那么如何设计rowkey?
当前认为rowkey不用来查询,也就是不需要存储相关的数据信息,只需要保证唯一性就可以了。关于使用rowkey来查询稍后在记录。
第一想法就是采用hash的方式来保证唯一性,例如我们采用两个字段来保证唯一性。
1结构:user_id + user_name + timestamp2hash前:1234sev7e015842525003hash后:591be696051b9bae8324eeda0ab43676
这里将拼接后的hash值作为rowkey,才用的MD5进行hsah,同样还可以采用其他的方式,sha1、sha256或sha512等算法。
这里使用hash能够满足将hash进行散列,打散数据集,但也存在问题就是hash后的长度过长,并不能满足rowkey尽可能的设计的短的要求。
rowkey设计要求:RowKey 可以是任意的字符串,最大长度64KB(因为 Rowlength 占2字节)。建议越短越好,原因如下:
- 数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光rowkey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;
- MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率;
- 目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性。
Reversing 的原理是反转一段固定长度或者全部的键。
1flink.iteblog.com moc.golbeti.knilf2www.iteblog.com ===> moc.golbeti.www3carbondata.iteblog.com moc.golbeti.atadnobrac
这些 URL 其实属于同一个域名,但是由于前面不一样,导致数据不在一起存放。我们可以对其进行反转,如上,经过这个之后,前缀就相同了,这些 URL 的数据就可以放一起了。
有效的打乱了行键,但是却牺牲了行排序的属性.
Salting的具体做法就是给原有的rowkey添加一个随机前缀,使得他与前边的rowkey排序时保持不同,这样在落入region时能够保证每一个region的均分数据。
1下边有两个rowkey2hash后:591be696051b9bae8324eeda0ab436763hash后:e696ae83b43051b9b591b67624eeda0a4
5加盐后:0001591be696051b9bae6加盐后:0002ae83b43051b9b591
以前一个hash值为例。为了满足rowkey尽可能短的以及16字节最优原则,我们截取hash值并给前边加一个后缀,这样就完成了一个salting操作。
为何要加0001、0002这类数字?这样做是为了后边我们在预分区时能够合理的设计预分区个数,和根据rowkey划分region。
salting后可以针对不同的region范围给定随机的前四位,保证数据均匀落入region,这能能够明显提高hbase写入的效率。不过前提是我们不需要rowkey参与查询,如果要更根据rowkey进行查找例如scan时,那么需要做更多的操作。
不过该种方式也是更多的被使用的。因为很多时候我们不需要根据rowkey进行查询,比如可以根据二级索引来进行检索,同时也有很多工具可以参考使用,例如Phoenix
和ElasticSearch
等。
1SQL+OLTP ==> Phonenix2全文检索+二级索引 ==> Solr/ES
参考 OpenTSDB 底层 HBase 的 Rowkey 是如何设计的
分析完rowkey的几种设计方案,接下来再回到第一个原因中提到解决默认一个region的办法就是预分区,也就是在创建表的根据数据量的大小预先设置好分区。这就涉及到如何设定一个合理的预分区个数,和根据rowkey划分region。
如何设计一个分区让所有的数据能够均匀分布?
例如我们打算针对table01
给其设计6个region,给每一个region一个独立的编号。
在数据写入时,我们知道hbase是根据rowkey的字典序进行排序的,也就说在每一个region内部其也是有序的,会根据不同region的边界(start rowkey -> end rowkey)将数据写入到对应的region上去,那么也就到了预分区最重要的一个步骤,如何合理均匀的划分region边界?
这里记录一个通用且有用的分区策略:将rowkey salting然后根据前边的随机数进行划分region。
如我们针对上边七个个region可以设计成:
1~ 0001|20001| 0002|30002| 0003|40003| 0004|50004| 0005|60005| 0006|7~ 0006|
为什么后面会跟着一个"|",是因为在ASCII码中,"|"的值是124,大于所有的数字和字母等符号,当然也可以用“~”(ASCII-126)。分隔文件的第一行为第一个region的endkey,每行依次类推,最后一行不仅是倒数第二个region的endkey,同时也是最后一个region的startkey。也就是说分区文件中填的都是key取值范围的分隔点。
这样每次生成rowkey时只需要在这六个数中随机选择一个将其拼接到hash值上,在写入时就会写入到我们规划的对应分区中去。
1加盐后:0000591be696051b9bae ===》一号分区2加盐后:0001ae83b43051b9b591 ===》二号分区3加盐后:0002ae96051b51b96051 ===》三号分区
下边记录两种预分区的方式:
我们可以在本地指定一个文件split-file.txt
10001|20002|30003|40004|50005|60006|
创建表时:
1hbase(main):008:0> create 'split-table','f1',{SPLITS_FILE => '/home/hadoopadmin/split-file.txt'}20 row(s) in 5.1390 seconds3
4=> Hbase::Table - split-table5hbase(main):009:0>
创建完成后可以在页面查看到:
创建预分区:
1public static void doPreRegion(Connection connection, HashMap<String,String> tableName, Integer regionNum,2 Boolean dropExistTable) throws IOException {3 Admin admin = connection.getAdmin();4 //支持同时创建多个table5 for (Map.Entry<String,String> entry : tableName.entrySet()){6 TableName name = TableName.valueOf(entry.getKey());7 HTableDescriptor hTableDescriptor = new HTableDescriptor(name);8 HColumnDescriptor hColumnDescriptor = new HColumnDescriptor(entry.getValue());9 hTableDescriptor.addFamily(hColumnDescriptor);10 byte[][] bytes = new byte[regionNum][];11 //生成每一个region的边界值 0000| 0001| 0002|。。。12 for (int i = 0; i < regionNum; i++) {13 String leftPad = StringUtils.leftPad(i + "", 4, "0");14 bytes[i] = Bytes.toBytes(leftPad+"|");15 }16 //当表名存在时可选是否drop原有的表17 if (admin.tableExists(name)){18 if (dropExistTable){19 logger.info("table {} exist, will drop it.", name);20 admin.disableTable(name);21 admin.deleteTable(name);22 }else {23 logger.warn("table {} exist!", entry.getKey());24 continue;25 }26
27 }28 //指定分区创建table29 admin.createTable(hTableDescriptor, bytes);30 logger.info("created hbase table {} completed!, with columnFamily {} and {} regions.", name,31 entry.getValue(), regionNum);32 }33 admin.close();34}
执行成功后就能够看到创建好的七个预分区。如上图。
获取rowkey:
1def getRowKey(str:String,numRegion:Int):String ={2 val result: Int = (str.hashCode & Integer.MAX_VALUE) % numRegion3 val prefix:String = StringUtils.leftPad(result+"",4,"0");4 val suffix: String = DigestUtils.md5Hex(str).substring(0,12)5 prefix + suffix6 }
本篇总结记录了在防止hbase产生热点问题时的解决方案,主要从两个方面进行分析,一设计合理的rowkey,将数据集进行合理的散列。二设计合理的预分区,将散列后的数据集均匀的插入到region中,以防止hbase产生数据倾斜等热点问题。后续遇到更好的设计方案再及时更新。
over~