HBase 中的 rowkey 是按字典顺序排序的, 通过 rowkey 查询可以对千万级的数据实现毫秒级响应. 然而, 如果 rowkey 设计不合理的话经常会出现一个很普遍的问题 ---- 热点. 当大量 client 的请求 (读或者写) 只指向集群的一个节点, 或者很少量的几个节点时, 也就代表产生了热点问题.
避免产生热点的方式也就是尽可能的将 rowkey 均匀分散到所有的 region 上, 下面介绍了几种 rowkey 设计常用的方式:
第一: 加盐(salting)
加盐是指在 rowkey 的前缀添加随机数据, 使 rowkey 尽可能的分布到其他 regionserver 上
假设遇到下面的 rowkey, 表的预分区设置为每个字母对应一个 region. 前缀 "a" 是一个 region, 前缀 "b" 是另一个 region 等等. 那么在这个表中, 所有以 "f" 开头的 rowkey 都将位于同一个 region. 比如:
- foo0001
- foo0002
- foo0003
- foo0004
那么, 如果你想把它们分散到四个不同的 region, 那么就可以使用四种不同的前缀: a,b,c 和 d 来做加盐. 在加盐之后, rowkey 也就变成了下面这样.
- a-foo0003
- b-foo0001
- c-foo0004
- d-foo0002
- (ps: 由于现在可以向四个 region 写数据, 理论上, 性能比之前向同一个 region 写吞吐量提升四倍)
并且, 如果后续有新的数据写入, rowkey 也就会随机的添加前缀, 写到不同的 region 中
缺点: 加盐虽然可以很大程度的避免热点问题, 提升写入效率, 但是由于 rowkey 被随机的添加了 salt 值, 在读取时候要付出额外的开销. 具体怎么读取加盐后的数据, 后面再做介绍
第二: 哈希(hashing)
哈希的算法有多种, 在 rowkey 设计中用的比较多的大概就是 MD5 了吧, 但是需要注意的是 MD5 散列还是有碰撞的可能性的, 概率很小, 但是不是零.
所以一般使用 MD5 做 rowkey 散列时候, 都会附加一个唯一字段, 比如账号字段 account, 对 account 做 MD5, 截取 6 位左右的 md5 返回值然后再拼接 account 字段, 也就是:
substr(md5(account))+account
此外, 通过 md5 散列之后的 rowkey, 在创建表预分区时候, 可以使用 hbase 自带的 HexStringSplit 方法
第三: 反转(Reversing)
如果定义的 rowkey 字段, 前部分数据变化幅度很小, 变化很慢, 尾部数据变化频率较高, 便可以考虑反转字段, 尤其对类似时间戳的数据
不管以哪种方式设计 rowkey, 在查询时候也要做对应的数据处理, 比如做 hash 的, 查询时候也需要先把数据 hash 之后, 然后查询 rowkey; 通过反转方式设计的 rowkey 同理.
第四: 最小化 rowkey 和列簇长度
rowkey 可以是任意的字符串, 最大长度 64KB, 但是建议在设计 rowkey 时候, 尽可能的短, 原因:
1.hbase 数据存储是以 key-value 的形式存储的, 如果 rowkey 比较长, 比如 100 字节, 那么 1000w 行数据, 光 rowkey 存储就需要 100*1000w=10 亿个字节, 将近 1G 的数据.
2.memstore 的会缓存数据到内存, 如果 rowkey 比较长, 同样会占用更多的空间
3. 建议 rowkey 设计在 8 字节的整数倍, 控制在 16 个字节, 因为目前的操作系统大多都是 64 位的, 整数倍更好了利用了操作系统的特性.
列簇 (ColumnFamily) 同理, 尽可能的短, 最好是一个字符, 比如 f 或者 d
第五: Byte Patterns
我们知道, long 类型是 8 个字节, 并且你可以通过 long 类型存储一个最大为 18,446,744,073,709,551,615 的无符号数字, 仅仅用 8 个字节, 但是如果以 string 类型的形式存储这样的数字, 那么几乎需要 3 倍空间的大小(假定每个字符占一个字节)
举个例子验证一下:
- // long
- //
- long l = 1234567890L;
- byte[] lb = Bytes.toBytes(l);
- System.out.println("long bytes length:" + lb.length); // returns 8
- String s = String.valueOf(l);
- byte[] sb = Bytes.toBytes(s);
- System.out.println("long as string length:" + sb.length); // returns 10
- // hash
- //
- MessageDigest md = MessageDigest.getInstance("MD5");
- byte[] digest = md.digest(Bytes.toBytes(s));
- System.out.println("md5 digest bytes length:" + digest.length); // returns 16
- String sDigest = new String(digest);
- byte[] sbDigest = Bytes.toBytes(sDigest);
- System.out.println("md5 digest as string length:" + sbDigest.length); // returns 26
但是, 也有一个缺点, 就是如果使用这种二进制表示的类型时候, 在 hbase shell 界面查数据的时候, 可读性比较差, 比如:
- hbase(main):002:0> get 'table1', 'rowkey1'
- COLUMN CELL
- f:q timestamp=1369163040570, value=\x00\x00\x00\x00\x00\x00\x00\x01
- 1 row(s) in 0.0310 seconds
来源: https://www.cnblogs.com/dtmobile-ksw/p/11379786.html