一、背景
- HBase表设计主要要注意两点,一是rowkey设计,而是列簇设计;
1.一分钟简介
- HBase是三维有序存储的,通过rowkey(行键),Column Family(列簇/列族),Column Qualifier(列) 这个三个维度可以对HBase中的数据进行快速定位。针对一个前面三个维度可以确定一个Cell,一个Cell可以有多个version,通过 TimeStamp(时间戳)确定,最新的时间戳排在最前最先取到;
- HBase中rowkey可以唯一标识一行记录,在HBase查询的时候,常见有以下2种方式:
- 通过get方式,指定rowkey获取唯一一条记录
- 通过scan方式,设置startRow和stopRow参数进行范围匹配(需要控制流量)
- 通过get方式,指定rowkey获取唯一一条记录
- 针对这两种方式,在第五项中,我们简要介绍了几种常见的rowkey设计思路(充分利用HBase的分布式特性,将请求分散到不同region上)
二、rowkey长度原则——短小精悍
-
通过rowkey能定位到具体数据,越短越好。不必要的数据不要存在rowkey中
- 原因如下:
- 节省磁盘、内存空间
- 从rowkey了解业务设计更清晰
三、rowkey顺序原则——精确&必选在前、尽量定位小范围
- 查询必填的精确的字段必须拼到rowkey最前面;(比如要实现类似 where A='xxx' and B='xx' and C like 'Pre%' 的需求 ,那么A,B应该在rowkey前面,C拼在A和B后面)
- 能过滤越多的字段,放到越靠前;能减少查询扫描量;
四、rowkey唯一原则——小心被覆盖
- rowkey必须保证业务上唯一,防止更新被覆盖
- 比如存城市下所有用户信息,rowkey不能只存城市id,得加上用户id,最终格式为: 01_driver001 ;如果要按时间查询,还要加上时间戳或者yyyy-MM-dd ,格式为: 01_driver001_2019-08-01
五、rowkey散列原则——形散神不散
-
HBase 通过一个表分多个region,不同region放在不同机器上,将数据请求分散到多台机器。如果不散列,就有产生热点(参见第五条)
-
rowkey前缀是散列的即可!不必须把整个rowkey打散,比如可以:[活动id]_beijing_01 ,这里后面的beijing , 01 可以按正常格式。
-
DHS上可以选择常见的region split方式,以下是常见的散列规则:
-
十六进制字符串表达——非常通用MD5
-
md5就是一个散列的结果
-
写入HBase之后通过hbase shell命令查看结果为:47bce5c74f589f4867dbd57e9ca9f808 ——是一串0~9 a~f的字符串,不是/x0a的格式哦
-
-
十进制字符串表达——id反转
-
例如手机号逆序; 递增的订单号逆序,逆序也是为了散列;
-
写入HBase之后通过hbase shell命令查看结果为:95962300581
-
注意:有些业务id逆序之后并不能散列;比如一些社交账号id可能有“靓号” ,好多账号最后几位都是6,8,9,66,88,99,可能逆序之后依然散列不了;那么请用前面的MD5方式!
-
-
二进制——高级用法,传说中的\x00
-
散列,然后再序列化之后的rowkey
-
写入HBase之后通过hbase shell命令查看结果为:\x0E\xA0\xEA\x0E\xA0\xEA\x0E\xA0
-
-
六、为啥要散列,减少热点
- HBase中的行是按照rowkey的字典顺序排序的,这种设计非常适应scan操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于scan。
- 然而糟糕的rowkey设计会使得过多的数据前缀都是一样的,从而形成热点;
- HBase建表可以预先划分数据的分布,我们期望这样程序刚启动的时候,数据就能自动分布到不同机器上充分利用资源。
- 赶羊的故事:好比羊圈管理员先盖好了N个羊圈,往N个羊圈赶羊,如果几个管理员预先协商的“策略”是按业务逻辑的“按羊的颜色区分”,估计最后结果是1个羊圈挤满了白色的羊,另外一个有几只黑羊,剩下8个没有羊(资源不均衡)
- 羊圈管理员思考之后:选择一个随机的策略,给羊加上编号 Sheep01...100,并且给编号加了一个MD5的前缀 md5(sheep01,0,3)_sheep01_其他查询用到字段,前缀是0000的去第一个羊圈,前缀是0001的去第2个...这样每个羊圈的羊数基本持平~~
- HBase建表可以预先划分数据的分布,我们期望这样程序刚启动的时候,数据就能自动分布到不同机器上充分利用资源。
- 大量访问会使热点region所在的单个机器超出自身承受能力,引起性能下降甚至region不可用
- 这也会影响同一个RegionServer上的其他region,由于主机无法服务其他region的请求。
- 设计良好的数据访问模式以使集群被充分,均衡的利用。
比如某业务id都是10开头的,如果不做散列的情况:
散列前,因为业务id不规律,不够分散,导致有热点
散列后,分散均匀
下面是一些常见的避免热点的方法以及它们的优缺点:
进阶的散列,加盐(scan扫描操作经常用到)
这里所说的加盐不是密码学中的加盐,而是增加salt_bucket机制,将一次请求分散到N个不同的region上。
常见场景:
- 比如要扫描 userId01开头的所有数据,可能有10w行
- 如果原来rowkey设计是:md5(userId),那么我们将要扫描10w行放在同一个机器的数据了,可能对这个机器压力非常大
- 修改一下rowkey,前面“加盐”,如下面表格:
00_userId01开头的数据 |
10_userId01开头的数据 |
20_userId01开头的数据 |
... |
90_userId01开头的数据 |
然后在客户端开10个线程,扫秒00开头_userId01的数据,10开头_userId01的数据...90开头_userId01的数据- 收集多个线程扫描结果,并进行处理
七、常见的rowkey设计问题
1.使用md5(业务id1)_业务id1_userId 还是 md5(业务id1)_userId
业务id1 是查询参数
如果已使用MD5 散列了业务id1 那在查询时就已知业务id了,没有必要在后面再写一遍业务id1了 可以参考上面【rowkey长度原则】
2.想将最新的数据先查出来
时间戳反转,一个特殊用法,按时间段查询,并且经常查询的数据是最新数据
想要根据用户id查询用户最近从start time到end time的行为,或者用户近一个小时的行为,并且将最新的数据先加载
- 1.用户id肯定要放到rowkey
- 2.时间肯定要放到rowkey
初步思路:md5(用户ID)_行为timestamp
进阶,要查“此用户近一个小时行为”,并且将最新的数据先加载
- 优化前:md5(用户ID)_行为timestamp
- 优化后:md5(用户ID)_【Long.Max_Value - 行为timestamp】
- 查询方法:startRow是md5(用户ID)_[Long.Max_Value - 结束时间],stopRow是md5(用户ID)_[Long.Max_Value - 起始时间],这样会先扫描到最新数据
八、列簇设计——减少IO影响
- 经常一起查的:可以合并到一个列簇下面避免扫过多的文件;
- 不经常一起查询出来的,就不要放到一个列簇了。否则扫5个字段的数据,只用3个字段。
- 例子:比如存用户信息。性别,年龄,地域经常一起被查询。这三个可以放一个列簇下面。
- 另外的列簇可以放:标签,用户级别等。