大白话彻底讲透 HBase Rowkey 设计和实现!

2021-02-08 20:59:18 浏览数 (1)

- 前言 -

大家都知道 HBase 由于它存储和读写的高性能,在 OLAP 即时分析中发挥着非常重要的作用,而 RowKey 作为 HBase 的核心知识点,其设计势必会影响到数据在 HBase 中的分布,甚至会影响我们查询的效率,可以说 RowKey 的设计质量关乎了 HBase 的质量。

言归正传,对于关系型数据库,数据定位可以理解为“二维坐标”;但在 HBase 中,定位一条数据(即一个Cell)我们需要4个维度的限定:行键(RowKey)、列族(Column Family)、列限定符(Column Qualifier)、时间戳(Timestamp)。

其中,RowKey 是最容易出现问题的。所以,除了根据业务和查询需求来设计之外,还有很多地方需要我们注意。

- RowKey 概念 -

HBase 中 RowKey 可以唯一标识一行记录,在 HBase 查询的时候有以下几种方式:

  1. 通过 get 方式,指定 RowKey 获取唯一一条记录;
  2. 通过 scan 方式,设置 startRow 和 stopRow 参数进行范围匹配;
  3. 全表扫描,即直接扫描整张表中所有行记录。

从字面意思来看,RowKey 就是行键的意思,在增删改查的过程中充当了主键的作用。它可以是任意字符串,在 HBase 内部 RowKey 保存为字节数组。

HBase 中的数据是按照 Rowkey 的 ASCII 字典顺序进行全局排序的,有伙伴可能对 ASCII 字典序印象不够深刻,下面举例说明:

假如有5个Rowkey:"012", "0", "123", "234", "3",按ASCII字典排序后的结果为:"0", "012", "123", "234", "3"。

因此我们设计RowKey时,需要充分利用排序存储这个特性,将经常一起读取的行存储放到一起,要避免做全表扫描,因为效率特别低。

- 什么是数据热点 -

1、热点现象产生

HBase 中的行是按照 Rowkey 的字典顺序排序的,这种设计优化了 scan 操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于 scan。

然而糟糕的 Rowkey 设计是热点的源头。热点发生在大量的 client 直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。

大量访问会使热点 region 所在的单个机器超出自身承受能力,引起性能下降甚至 region 不可用,这也会影响同一个 RegionServer 上的其他 region,由于主机无法服务其他 region 的请求,这样就造成 数据热点现象(这一点其实和数据倾斜类似)。

所以我们在向 HBase 中插入数据的时候,应优化 RowKey 的设计,使数据被写入集群的多个 Region,而不是一个。尽量均衡地把记录分散到不同的 Region 中去,平衡每个 Region 的压力。

2、避免数据热点的方法

在日常使用中,主要有 3 个方法来避免热点现象,分别是反转、加盐和哈希,下面咱们逐个举例分析:

(1)反转(Reversing)

第一种要分析的方法是反转,顾名思义,它就是把固定长度或者数字格式的 RowKey 进行反转,反转分为一般数据反转和时间戳反转,其中以时间戳反转较常见:

  • 反转固定格式的数值以手机号为例,手机号的前缀变化比较少(如 152、185 等),但后半部分变化很多。如果将它反转过来,可以有效地避免热点。不过其缺点就是失去了有序性。
  • 反转时间这个操作严格来讲不算“打散”,但可以调整数据的时间排序。如果将时间按照字典序排列,最近产生的数据会排在旧数据后面。如果用一个大值减去时间(比如用 99999999 减去 yyyyMMdd,或者 Long.MAX_VALUE 减去时间戳),最新的数据就可以排在前面了。

(2)加盐(Salting)

这里的“加盐”与密码学中的“加盐”不是一回事。它是指在 RowKey 的前面增加一些前缀,加盐的前缀种类越多,RowKey 就被打得越散。

需要注意的是,分配的随机前缀的种类数量应该和我们想把数据分散到的那些 region 的数量一致。只有这样,加盐之后的 Rowkey 才会根据随机生成的前缀分散到各个 region 中,避免了热点现象。

(3)哈希(Hashing)

其实哈希和加盐的适用场景类似,但我们前缀不可以是随机的,因为必须要让客户端能够完整地重构 RowKey。所以一般会拿原 RowKey 或其一部分计算 Hash 值,然后再对 Hash 值做运算作为前缀。

- RowKey 的设计原则 -

通过前面的分析,我们知道了 HBase 中 RowKey 设计的重要性。为了帮助我们设计出完美的 RowKey,HBase 提出了 RowKey 的设计原则主要有以下四点:长度原则、唯一原则、排序原则、散列原则。

1、RowKey 长度原则

RowKey 是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际应用中一般为 10-100bytes,以 byte[] 形式保存,一般设计成定长。

建议越短越好,不要超过 16 个字节,原因如下:

  • 在 HBase 的底层存储 HFile 中,RowKey 是 KeyValue 结构中的一个域。假设 RowKey 长度 100B,那么 1000 万条数据中,光 RowKey 就占用掉 100*1000w=10 亿个字节 将近 1G 空间,这样会极大影响 HFile 的存储效率。

HFile 简单结构示意

  • HBase 中设计有 MemStore 和 BlockCache,分别对应列族 /Store 级别的写入缓存,和 RegionServer 级别的读取缓存。如果 RowKey 字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。

另外,我们目前使用的服务器操作系统都是 64 位系统,内存是按照 8B 对齐的,因此设计 RowKey 时一般做成 8B 的整数倍,如 16B 或者 24B,可以提高寻址效率。

同样,列族、列名的命名在保证可读的情况下也应尽量短。value 永远和它的 key 一起传输的。当具体的值在系统间传输时,它的 RowKey,列名,时间戳也会一起传输(因此实际上列族命名几乎都用一个字母,比如‘c’或‘f’)。如果你的 RowKey 和列名和值相比较很大,那么你将会遇到一些有趣的问题。Hfile 中的索引最终占据了 HBase 分配的大量内存。

2、唯一原则

其实唯一原则咱们可以结合 HashMap 的源码设计或者主键的概念来理解,由于 RowKey 用来唯一标识一行记录,所以必须在设计上保证 RowKey 的唯一性。

需要注意:由于HBase中数据存储的格式是Key-Value对格式,所以如果向HBase中同一张表插入相同RowKey的数据,则原先存在的数据会被新的数据给覆盖掉(和HashMap效果相同)。

3、排序原则

RowKey 是按照字典顺序排序存储的,因此,设计 RowKey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。

一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为 RowKey 的一部分对这个问题十分有用,可以用 Long.Max_Value-timestamp 追加到 key 的末尾。

例如 [key][reverse_timestamp],[key] 的最新值可以通过 scan [key]获得[key] 的第一条记录,因为 HBase 中 RowKey 是有序的,第一条记录是最后录入的数据。

4、散列原则

散列原则用大白话来讲就是,咱们设计出的 RowKey 需要能够均匀的分布到各个 RegionServer 上。

比如设计 RowKey 的时候,当 Rowkey 是按时间戳的方式递增,就不要将时间放在二进制码的前面,可以将 Rowkey 的高位作为散列字段,由程序循环生成,可以在低位放时间字段,这样就可以提高数据均衡分布在每个 Regionserver 实现负载均衡的几率。

结合前面分析的热点现象的起因思考:如果没有散列字段,首字段只有时间信息,那就会出现所有新数据都在一个RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer上,降低查询效率。

- 实际场景 -

实际业务中,有一部分是用户在日历上记录自己的行为。需要储存在 RowKey 中的维度有:用户 ID(uuid,不会超过十亿)、日历上的日期(date,yyyyMMdd格式)、记录行为的类型(type,0~99之间)。

记录的详细数据则存储在列 f:data 中。根据查询逻辑,我们可以设计的 RowKey 格式如下:

代码语言:javascript复制
9~79809782~05~0008839540

长度正好是 24B。以字符‘~’为分界(‘~’的ASCII码是最大的,方便),各个部分的含义如下:

  • uuid.toString().hashCode() % 10
  • 99999999 - date
  • StringUtils.leftPad(type, 2, "0")
  • StringUtils.leftPad(uuid, 10, "0")

0 人点赞