以往工作中HBase存储海量数据时候,因为历史原因主键使用自增长序列,数据迁移到HBase中时,并没有改变主键策略。导致数据全部写入一个region,造成数据热点。当时也没有采用预分区,数据增长过快,region过大时,在系统低负载时段需手动切分region。这些原因导致集群整体效率低。
现在要求
目前正好有一个新的数据存储场景开始设计,对数据需要增量顺序读(如:增量构建全文索引等),中等规模随机读。能按照区间范围顺序查询,并能控制查询的开始和结束。以方便增量或重新构建索引或控制区间化数据读取需求等等。总结历史经验和教训, 现在需要考虑的2种场景:
- 海量文本数据A,天入库数据量在30-200W左右,文本格式长,需要保存原文。(数量现对小,单文本容量比较大,需要保存原文)
- 海量文本数据B,天数据量在500w-5000w之间,文本格式短小。(数量现对多,单文本容量相对小 )
HBase基本情况
表和索引组成
HBase一般由行键(row key)、列键(column qualifier )、列族(column family)组成。行键对应关键数据库中主键,HBase为行键建立了索引,列键归属列族。通过行键/列键/列族定位到一个唯一记录。
HBase中使用.META内部表存储region的分布情况以及每个region的详细信息。region中记录了rowkey范围,region分散在不同服务器中。通过region server提供访问数据访问服务,region server可服务多个region,来自不同region server上的region组合成表格的整体逻辑视图。
获取记录方式
- 通过get方式,指定rowkey获取唯一记录
- 通过scan方式,设置startRow和stopRow参数进行范围匹配
- 通过scan方式,全表扫描,并通过RowFilter过滤出数据
基本可归纳为顺序读和随机读。
rowkey原则
长度原则
行键长度尽量短和合理,因为持久化文件HFile中使用KeyValue形式保存数据,column family/column qualifier/rowkey会记录到每一条数据中,导致存储文件过大,也会导MemStore内存有效利用率降低。rowkey使用byte[]保存数据,使用数值(long)比字符(String)占用更小空间。64为系统内存8字节对齐,控制在16个字节,尽量使用8为倍数。
散列原则避免热点
- 加盐
在rowkey前按规则加随机数,使数据分散到不同region上避免热点。也可通过业务规则分段比如:userid-service-timestamp,把不同userid或不同业务数据切分到不同region。 - 哈希
生成哈希或使用UUID散列它。 - 反转
如果是顺序序列可以反转它,让他经常改变的部分排到前面避免数据集中。时间反转也可以考虑Long.Max_Value - timestamp。
唯一原则
主键都必须保证唯一。
分布式主键算法
分布式主键算法要求
- 毫秒级的快速响应
- 可用性强
- prefix有连续性方便DB顺序存储
- 体积小,8字节为佳
目前分布式主键算法比较
UUID
16字节,JAVA自带,好用,占用空间大。
Twitter Snowflake
Snowflake: timestamp + work id + seq number
8字节,可用性强,速度快。占用空间小,如果考虑复杂环境work id需要更好处理。twitter默认实现需要引入zookeeper 和独立的snowflake专用服务器,UC实现通过配置文件确定work id。
MongoDB ObjectId
ObjectId:timestamp + machine + pid + seq number
12字节,可用性强,速度快。占用空间中等,用空间降低实现复杂度,基本没有其他依赖。
业务rowkey设计
业务的需求
- 大文本A每分钟最多1388条,每秒23条。
- 小文本B每分钟最多34722条,每秒578条。
考虑到大量顺序读,需要做到局部连续,全局分散。每秒极端情况写数据量不多,可考虑按照分钟分区。一共60个分区。获取一天数据时候通过60*24=1440 按照1440个局部连续批来获取数据。
rowkey规则1
参考snowflake和ObjectId原理,感觉它并不好直观分区,所以:
partition + timestamp + work id+ seq number 增加分区,减少时间和work id范围。
0-000000-01111111 10111111 11111000 00111100-00000000 0-00000000 00000000
1bit 不用
6bit 分区,可支持63个分区,可使用秒或分钟做分区
32bit 时间 System.currentTimeMillis()到秒(可以用到2099年)
9bit 区分机器和进程,需要在存储空间和复杂度上找平衡
16bit seq(最大65535)
全长64Bit,8Byte可做到不依赖其他服务。
分区可表示为:
0-000000-0000000 000000000 00000000 00000000-00000000 0-00000000 00000000
0-000001-0000000 000000000 00000000 00000000-00000000 0-00000000 00000000
0-000010-0000000 000000000 00000000 00000000-00000000 0-00000000 00000000
0-000011-0000000 000000000 00000000 00000000-00000000 0-00000000 00000000
rowkey规则2
规则1太复杂实现和可读性低简,简化下
reverse timestamp(mmHHddMMyyyy)+ seq number(0-99999)
seq number使用redis保证全局唯一,每个客户端使用步长减少redis访问频次。
例如:rowkey=20170719213012345表示为:30211907201712345使用分钟做分区30表示分区。
分区可表示为:
1000000000000000L:11100011010111111010100100110001101000000000000000
3000000000000000L:111000110101111110101001001100011010000000000000000
59000000000000000L:11010001100111000010111111111001101111111000000000000000
56Bit,7Byte 依赖redis服务 简单直观可读性好。
rowkey规则2顺序读取数据方式
假如我需要scan查询2017/07/19数据,我需要从201707190000~201707192359
60*24=1440做循环。2017/07/19/ 21:35为例子, StartRow和StopRow设置如下:
StartRow=35211907201700000L
StopRow=35211907201799999L
最终选择规则2作为rowkey规则,随然依赖redis服务,但是存储空间小、可读性高、可理解性好、方便使用和维护。
其他
不考虑HBase RowFilter方式,希望的效果就是直接利用rowkey内部索引和.META表。对于其他复杂组合查询,我倾向使用全文索引ES或Solr。
可以参考算法源码
UC Snowflake
https://github.com/sumory/uc/blob/master/src/com/sumory/uc/id/IdWorker.java
MaongoDB ObjectID
https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/types/ObjectId.java
Email:[email protected]
刘威 2017年7月19日 长沙