一、出现热点问题原因
1、hbase的中的数据是按照字典序排序的,当大量连续的rowkey集中写在个别的region,各个region之间数据分布不均衡;
2、创建表时没有提前预分区,创建的表默认只有一个region,大量的数据写入当前region;
3、创建表已经提前预分区,但是设计的rowkey没有规律可循,设计的rowkey应该由regionNo+messageId组成。
二、如何解决热点问题
设计可以让数据分布均匀的rowkey,与nosql数据库们一样,rowkey是用来检索记录的主键。访问hbase table中的行,rowkey 可以是任意字符串(最大长度 是 64KB,实际应用中长度一般为 10-100bytes),在hbase内部,rowkey保存为字节数组,存储时,数据按照rowkey的字典序排序存储。
创建表命令:
create 'testTable',{NAME => 'cf', DATA_BLOCK_ENCODING => 'NONE', BLOOMFILTER => 'ROW', REPLICATION_SCOPE=> '0', VERSIONS => '1', COMPRESSION => 'snappy', MIN_VERSIONS =>'0', TTL => '15552000', KEEP_DELETED_CELLS => 'false', BLOCKSIZE =>'65536', IN_MEMORY => 'false', BLOCKCACHE => 'true', METADATA =>{'ENCODE_ON_DISK' => 'true'}},{SPLITS_FILE=>'/app/soft/hbaseregionsplist/region.txt'}
region.txt内容:
我这里预分10个region,创建表之后,在hbae的ui中可以看到以下信息,说明分预期ok了!!!
1、第一种设计rowkey方式:随机数+messageId,如果想让最近的数据快速get到,可以将时间戳加上
我这里的region是0001|到0009|开头的,因为hbase的数据是字典序排序的,所以如果我生成的 rowkey=0002rer4343343422,则当前这条数据就会保存到0001|~0002|这个region里,使用了|,因为我的messageId都是字母和数字,|的ASCII值大于字母和数字。
regionNum=10,因为我预分10个region
这种设计的rowkey可以解决热点问题,但是要建立关联表,比如将rowkey保存到数据库或者nosql数据库中,因为前面的regionNo是随机的,不知道 对应数据在hbase的rowkey是多少;同一批数据,因为这个regionNo是随机的,所以要到多个region中get数据,不能使用startkey和endkey去get数据。
2、第二种设计rowkey的方式:通过messageId映射regionNo,这样既可以让数据均匀分布到各个region中,同时可以根据startkey和endkey可以get到同一批数据
messageId映射regionNo,使用一致性hash算法解决,一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,参考(https://baike.baidu.com/item/%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C/2460889?fr=aladdin)
(https://www.cnblogs.com/lpfuture/p/5796398.html)
public class ConsistentHash<T> implements Serializable{ private static final long serialVersionUID = 1L; private final HashFunction hashFunction; //每个regions的虚拟节点个数 private final int numberOfReplicas; //存储虚拟节点的hash值到真实节点的映射 private final SortedMap<Long, String> circle = new TreeMap<Long, String>(); public ConsistentHash(HashFunction hashFunction, int numberOfReplicas, Collection<String> nodes) { this.hashFunction = hashFunction; this.numberOfReplicas = numberOfReplicas; for (String node : nodes){ add(node); } } /** * 添加节点 * @param node * @see java.util.TreeMap * */ public void add(String node) { for (int i = 0; i < numberOfReplicas; i++) /* * 不同的虚拟节点(i不同)有不同的hash值,但都对应同一个实际机器node * 虚拟node一般是均衡分布在环上的,数据存储在顺时针方向的虚拟node上 */ circle.put(hashFunction.getHashValue(node.toString() + i), node); } /** * 移除节点 * @param node * @see java.util.TreeMap * */ public void remove(String node) { for (int i = 0; i < numberOfReplicas; i++) circle.remove(hashFunction.getHashValue(node.toString() + i)); } /** * 获取对应key的hashcode值,然后根据hashcode获取当前数据储存的真实节点 * */ public String get(Object key) { if (circle.isEmpty()) return null; //获取对应key的hashcode值 long hash = hashFunction.getHashValue((String) key); //数据映射在两台虚拟机器所在环之间,就需要按顺时针方向寻找机器 if (!circle.containsKey(hash)) { SortedMap<Long, String> tailMap = circle.tailMap(hash); hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); } return circle.get(hash); } /** * 获取hash环节点大小 * @return * */ public long getSize() { return circle.size(); } /** * 获取double类型数据的小数位后四位小数 * @param num * @return * */ public String getDecimalPoint(double num){ DecimalFormat df = new DecimalFormat("0.0000"); return df.format(num); }
}
public class HashFunction implements Serializable{ private static final long serialVersionUID = 1L; /** * 获取对应字符串的hashCode值 * @param key * @return * */ public long getHashValue(String key) { final int p = 167776199999; int hash = (int) 216613626111L; for (int i = 0; i < key.length(); i++) hash = (hash ^ key.charAt(i)) * p; hash += hash << 13; hash ^= hash >> 8; hash += hash << 3; hash ^= hash >> 18; hash += hash << 5; // 如果算出来的值为负数则取其绝对值 if (hash < 0) hash = Math.abs(hash); return hash; } }
这样可以通过messageId映射出regionNo,最后得到rowkey。
我目前满意第二种方式,然后在es中建立关联表,get数据时,现在es中get到rowkey,然后在hbase中获取数据,这个根据自己的业务设计。
写的内容有问题,欢迎来吐槽,我会及时修改,谢谢!