HBase 是三维有序存储的,通过 rowkey(行键),column key(column family 和 qualifier)和 TimeStamp(时间戳)这个三个维度可以对 HBase 中的数据进行快速定位。
HBase 中 rowkey 可以唯一标识一行记录,在 HBase 查询的时候,有以下几种方式:
• 通过get方式,指定rowkey获取唯一一条记录;
• 通过 scan 方式,设置 startRow 和 stopRow 参数进行范围匹配;
• 全表扫描,即直接扫描整张表中所有行记录;
1. Rowkey 长度原则
rowkey 是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际应用中一般为10-100bytes,以 byte[] 形式保存,一般设计成定长。
建议越短越好,不要超过 16 个字节,原因如下:
• 数据的持久化文件 HFile 中是按照 KeyValue 存储的,如果 rowkey 过长,比如超过 100 字节,1000w 行数据,光 rowkey 就要占用 100*1000w=10 亿个字节,将近 1G 数据,这 样会极大影响 HFile 的存储效率;
• MemStore 将缓存部分数据到内存,如果 rowkey 字段过长,内存的有效利用率就会 降低,系统不能缓存更多的数据,这样会降低检索效率;
• 目前操作系统都是 64 位系统,内存 8 字节对齐,控制在 16 个字节,8 字节的整数倍 利用了操作系统的最佳特性。
2.RowKey 散列原则
必须在设计上保证 RowKey 唯一性,RowKey 是按照字典顺序排序存储的,因此,设计 RowKey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。
3. RowKey 唯一原则
必须在设计上保证 RowKey 唯一性,RowKey 是按照字典顺序排序存储的,因此,设计 RowKey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能 会被访问的数据放到一块。
4. 针对热点问题的 RowKey 设计原则
HBase 中的行是按照 RowKey 的字典顺序排序的,这种设计优化了 scan 操作,可以将 相关的行以及会被一起读取的行存取在临近位置,便于 scan。然而糟糕的 Region 设计是热点的源头。 热点发生在大量的 client 直接访问集群的一个或极少数个节点(访问可能是读, 写或者其他操作)。大量访问会使热点 Region 所在的单个机器超出自身承受能力,引起性 能下降甚至 Region 不可用,这也会影响同一个 RegionServer 上的其他 Region,由于主机无 法服务其他 Region 的请求。 设计良好的数据访问模式以使集群被充分,均衡的利用。
为了避免写热点,设计 RowKey 使得不同行在同一个 Region,但是在更多数据情况下, 数据应该被写入集群的多个 Region,而不是一个。
下面是一些常见的避免热点的方法以及它们的优缺点:
4.1 加盐
这里所说的加盐不是密码学中的加盐,而是在 rowkey 的前面增加随机数,具体就是给 rowkey 分配一个随机前缀以使得它和之前的 rowkey 的开头不同。分配的前缀种类数量应该 和你想使用数据分散到不同的 region 的数量一致。加盐之后的 rowkey 就会根据随机生成的 前缀分散到各个 region 上,以避免热点。加随机数可以使数据均匀分布,但是不可重构, 失去 get 快速定位数据的能力。
4.2 哈希
哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是 可以预测的。使用确定的哈希可以让客户端重构完整的 rowkey,可以使用 get 操作准确获取 某一个行数据。
4.3 反转
第三种防止热点的方法时反转固定长度或者数字格式的 rowkey。这样可以使得 rowkey 中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机 rowkey,但是牺 牲了 rowkey 的有序性。
反转 rowkey 的例子以手机号为 rowkey,可以将手机号反转后的字符串作为 rowkey,这 样的就避免了以手机号那样比较固定开头导致热点问题。
4.4 时间戳反转
一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为 RowKey 的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp 追加到 key 的末尾(由 于 Hbase 数据按照 RowKey 自然排序,timestamp 越大代表数据越新,Long.Max_Value – timestamp 就越小,就可以保证最新的数据排在 ),例如 [key][ Long.Max_Value - 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 - 结束时间]。
5. 组合 RowKey 设计原则
RowKey 为多个属性拼接而成时,将具有高标识度的、经常使用检索列的属性放在组合RowKey 的前面。
6. 应用场景分析
6.1 针对事务数据 RowKey 设计
事务数据是带时间属性的,建议将时间信息存入到 Rowkey 中,这有助于提示查询检索 速度。对于事务数据建议缺省就按天为数据建表,这样设计的好处是多方面的。按天分表后, 时间信息就可以去掉日期部分只保留小时分钟毫秒,这样 4 个字节即可搞定。加上散列字段 2 个字节一共 6 个字节即可组成唯一 Rowkey。如下图所示:
这样的设计从操作系统内存管理层面无法节省开销,因为 64 位操作系统是必须 8 字节 对齐,但是对于持久化存储中 Rowkey 部分可以节省 25%的开销。也许有人要问为什么不将 时间字段以主机字节序保存,这样它也可以作为散列字段了。这是因为时间范围内的数据还 是尽量保证连续,相同时间范围内的数据查找的概率很大,对查询检索有好的效果,因此使 用独立的散列字段效果更好,对于某些应用,我们可以考虑利用散列字段全部或者部分来存 储某些数据的字段信息,只要保证相同散列值在同一时间(毫秒)唯一。
6.2 针对统计数据的 RowKey 设计
统计数据也是带时间属性的,统计数据最小单位只会到分钟(到秒预统计就没意义了)。 同时对于统计数据我们也缺省采用按天数据分表,这样设计的好处无需多说。按天分表后, 时间信息只需要保留小时分钟,那么 0~1400 只需占用两个字节即可保存时间信息。由于统 计数据某些维度数量非常庞大,因此需要 4 个字节作为序列字段,因此将散列字段同时作为 序列字段使用也是 6 个字节组成唯一 Rowkey。如下图所示:
6.3 针对通用数据的 RowKey 设计
通用数据采用自增序列作为唯一主键,用户可以选择按天建分表也可以选择单表模式。这种模式需要确保同时多个入库加载模块运行时散列字段(序列字段)的唯一性。可以考虑给不同的加载模块赋予唯一因子区别。设计结构如下图所示。