一、背景
HBase是三维有序存储的,通过rowkey(行键),column key(column family和qualifier)和Timestamp(时间戳)三个维度可以对hbase中的数据进行快速定位。RowKey可以唯一标识一行记录,在HBase查询的时候,常见有以下两种方式:
1)通过get方式,指定rowkey获取唯一一条记录。
2)通过scan方式,设置startRow和stopRow参数进行范围匹配(需要控制流量)。
二、设计原则
2.1 长度原则
通过rowkey能定位到具体数据,越短越好。不必要的数据不要存在rowkey中,节省磁盘,内存空间;清晰反应业务设计。
2.2 散列原则
hbase通过一个表分多个region,不同region放在不同机器上,将数据请求分散到多台机器。如果不散列,就会产生热点,具体为什么,下面会说。
以下是常见的散列规则:
十六进制:md5(id),md5值就是一个散列的结果。
二进制:散列并序列化之后的rowkey。
十进制:手机号逆序;逆序也是为了散列。
2.3 唯一原则
必须在设计上保证其唯一性。
三、热点问题
HBase中的行是按照rowkey的字典顺序排序的,这种设计优化了scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。然而糟糕的rowkey设计是热点的源头。 热点发生在大量的client直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用,这也会影响同一个RegionServer上的其他region,由于主机无法服务其他region的请求。 设计良好的数据访问模式以使集群被充分,均衡的利用。为了避免写热点,设计rowkey使得不同行在同一个region,但是在更多数据情况下,数据应该被写入集群的多个region,而不是一个。
四、HBase表设计图解
4.1 Get场景
为什么要对业务字段进行散列?因为需要负载均衡,可以将记录平均分到不同的region中。比如,业务id有时候开头是跟业务相关的一些编码,有可能某个编码下面对应记录比较多,某些编码对应比较少,比如手机号都是1xx开头的,这就有可能造成热点问题,那么如何避免热点问题,往后看。
4.2 Scan场景
利用hbase表,按照字典序排序的特性,进行扫描操作,要查询的数据有共同的前缀。当然,scan操作不是无限制的,目前最佳场景是每次scan 1w条一下的分散rowkey。
适合场景:
时序需求;写入用户id + 时间戳对应时间的轨迹点经纬度信息。
rowkey设计:散列(用户id) + 时间戳
不适合场景:
一次扫描,startKey、endKey之间的数据有10k甚至更多的场景。
HBase最优场景是startkey分散的,每次扫描在1000以内的场景。Scan每次最大扫描到1w已经是比较重的操作了。再大的scan可能需要用户申请的机器数量比较多,或许其他存储会更适合。
scan操作一般都设置startkey,stopkey。数据量比较多的时候(比如1w以上,2w以内)要增加salt_bucket
防止超时:scan 1w条以上的操作,要分成多次进行scan。因为scan数据操作会有大量的磁盘读取操作(磁盘IO升高),把结果放入内存(占用内存导致GC频率升高),再通过RPC发送到客户端(网络带宽)。
Filter进行过滤:即使设置了Filter,也会扫描startkey到stopkey之间的所有数据。再在内存中过滤之后再返回客户端。
下面是增加salt_bucket之后,分多个线程同时scan的情况。如果没有分salt_bucket,就是其中一个scan线程的情况。
五、如何避免热点
5.1 加盐
增加salt_bucket机制,将一次请求分散到N个不同的region上。
常见场景:比如原本要扫描 userId01开头的所有数据
现在改为分多个线程,扫描
00_userId01开头的数据
01_userId01开头的数据
...
99_userId01开头的数据
然后在客户端收集多个线程扫描结果,并进行处理。
5.2 哈希
将数据的get / 小scan 操作分散到不同region。哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据
常见的场景1:(通过MD5进行哈希)
查询需求:每次通过一个 完整的业务id ,查出这条业务id的各种属性
rowkey可以设计为:MD5(业务id) value:业务id各项属性
其中MD5就是一种哈希方式:
因为hbase按字典序排序,为了防止业务id在设计上有一些特殊格式设计,比如某一个值开头的id会非常多(相同前缀开头的rowkey会落到同一region/相邻region,导致这个region请求量远大于其他region),可以将 业务id 进行md5之后,放到rowkey中。
常见的场景2:反转字符串进行哈希
第三种防止热点的方法时反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
常见的场景:对手机号反转时候region split方式选“十进制”,填写region数量,表就会按0000~9999平均分成N个region
5.3 时间戳反转
按时间段查询,并且经常查询的数据是最新数据。一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为rowkey的一部分对这个问题十分有用,可以用Long.Max_Value - timestamp
追加到key的末尾,例如[key][reverse_timestamp],
[key]
的最新值可以通过scan [key]获得[key]的第一条记录,因为HBase中rowkey是有序的,第一条记录是最后录入的数据。
比如需要保存一个用户的操作记录,按照操作时间倒序排序,在设计rowkey的时候,可以这样设计
[userId反转][Long.Max_Value - timestamp],在查询用户的所有操作记录数据的时候,直接指定反转后的userId,startRow是[userId反转][000000000000],stopRow是[userId反转][Long.Max_Value - timestamp]
如果需要查询某段时间的操作记录,startRow是[user反转][Long.Max_Value - 结束时间],stopRow是[userId反转][Long.Max_Value - 起始时间],来查询到对应userId最新的记录
其他:
> rowkey、列名和每行value尽量短,因为会在查询时一起传输,占用网络和内存资源
> 尽量减少行和列的大小在HBase中,value永远和它的key一起传输的。当具体的值在系统间传输时,它的rowkey,列名,时间戳也会一起传输。如果你的rowkey和列名很大,甚至可以和具体的值相比较,那么你将会遇到一些有趣的问题。HBase storefiles中的索引(有助于随机访问)最终占据了HBase分配的大量内存,因为具体的值和它的key很大。可以增加block大小使得storefiles索引再更大的时间间隔增加,或者修改表的模式以减小rowkey和列名的大小。压缩也有助于更大的索引。
> 列族尽可能越短越好,最好是一个字符。
> 冗长的属性名虽然可读性好,但是更短的属性名存储在HBase中会更好。
Author:忆之独秀
Email:[email protected]
注明出处:https://blog.csdn.net/lavorange/article/details/87193449