RowKey 作为 HBase 的核心知识点,RowKey 设计会影响到数据在 HBase 中的分布,还会影响我们查询效率,所以 RowKey 的设计质量决定了 HBase 的质量。是大数据从业者必知必会的,自然也是面试必问的考察点。

那么 Rowkey 到底是什么呢?原理是什么呢?怎么设计 RowKey 呢?使用场景是怎样的呢?有哪些设计原则呢?又如何进行优化呢?

下面就让我们带着这些问题,一起探索 RowKey 的世界!

1.RowKey 的概念

RowKey 从字面意思来看是行键的意思,咱们知道 HBase 可以理解为一个nosql(not only sql)数据库,既然是数据库,那么咱们日常使用最多的就是增删改查(curd)。其实在增删改查的过程中 RowKey 就充当了主键的作用,它和众多的 nosql 数据库一样,可以唯一的标识一行记录。

RowKey 行键 (RowKey)可以是任意字符串,在 HBase 内部,RowKey 保存为字节数组。存储时,数据按照 RowKey 的字典序(byte order)排序存储。设计RowKey时,要充分利用排序存储这个特性,将经常一起读取的行存储放到一起。

RowKey 的特点小结如下:

  1. RowKey 类似于主键,可以唯一的标识一行记录;
  2. 由于数据按照 RowKey 的字典序(byte order)排序存储,因此 HBase 中的数据永远都是有序的。
  3. RowKey 可以由用户自己指定,只要保证这个字符串不重复就可以了。

知识点补充:在 HBase 中检索数据时使用到 RowKey 的一共有三种方式:

  • get:通过指定单个RowKey来获取对应的唯一一条记录;
  • like:通过RowKey的range来进行匹配;
  • scan:通过设置startRow和stopRow参数来进行范围匹配(注意:如果不设置就是全表扫描)。

2.RowKey 的作用

要了解 RowKey 的作用,首先我们需要知道在 HBase 中,一个 Region 就相当于一个数据分片,每个 Region 都有 StartRowKey 和 StopRowKey(用来表示 Region 存储的 RowKey 的范围),HBase 表里面的数据是按照 RowKey 来分散存储到不同的 Region 里面的。

为了避免热点现象咱们需要将数据记录均衡的分散到不同的 Region 中去,因此需要 RowKey 满足这种散列的特点。此外,在数据读写过程中也是与 RowKey 密切相关的。RowKey 的作用可以归纳如下:

  1. Hbase 在读写数据时需要通过 RowKey 找到对应的 Region;
  2. MemStore 和 HFile 中的数据都是按照 RowKey 的字典序排序。

那到底啥是热点现象呢?咱们接着分析!

3.热点现象

3.1 热点现象怎么产生

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

然而万事万物都有两面性,在咱们实际生产中,当大量请求访问 HBase 集群的一个或少数几个节点,造成少数 RegionServer 的读写请求过多,负载过大,而其他 RegionServer 负载却很小,这样就造成热点现象(吐槽:其实和数据倾斜类似,还整这么高大上的名字)。

掌握了热点现象的概念,我们就应该知道大量的访问会使热点 Region 所在的主机负载过大,引起性能下降,甚至导致 Region 不可用。所以我们在向 HBase 中插入数据的时候,应优化 RowKey 的设计,使数据被写入集群的多个 region,而不是一个。尽量均衡地把记录分散到不同的 Region 中去,平衡每个 Region 的压力。

其实 RowKey 的优化主要就是在解决怎么避免热点现象。那么有哪些避免热点现象的方法呢?各有什么缺点?带着问题,接着往下看。

3.2 如何避免热点现象(RowKey的优化)

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

3.2.1 反转(Reversing)

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

适用场景:

比如咱们初步设计出的 RowKey 在数据分布上不均匀,但 RowKey 尾部的数据却呈现出了良好的随机性(注意:随机性强代表经常改变,没意义,但分布较好),此时,可以考虑将 RowKey 的信息翻转,或者直接将尾部的 bytes 提前到 RowKey 的开头。

反转可以有效的使 RowKey 随机分布,但是反转后有序性肯定就得不到保障了,因此它牺牲了 RowKey 的有序性。

缺点:

利于 Get 操作,但不利于 Scan 操作,因为数据在原 RowKey 上的自然顺序已经被打乱。

举例:

比如咱们通常会有需要快速获取数据的最近版本的数据处理需求,这时候就需要把时间戳作为 RowKey 来查询了,但是时间戳正常情况下是这样的:

1588610367373
1588610367396
12

前面这部分是相同的,在查询的时候就容易造成热点现象,因此需要使用时间戳反转的方式来处理。实际生产中可以用 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 是[反转后的userId[Long.Max_Value - 起始时间], stopRow 是[反转后的userId][Long.Max_Value - 结束时间]

3.2.2 加盐(Salting)

第二种咱们要介绍的方法是加盐,玩过密码学的可能知道密码学里也有加盐的方法,但是咱们 RowKey 的加盐和密码学不一样,它的原理是在原 RowKey 的前面添加固定长度的随机数,也就是给 RowKey 分配一个随机前缀使它和之前的 RowKey 的开头不同。

适用场景:

比如咱们设计的 RowKey 是有意义的,但是数据类似,随机性比较低,反转也没法保证随机性,这样就没法根据 RowKey 分配到不同的 Region 里,这时候就可以使用加盐的方式了。

需要注意随机数要能保障数据在所有 Regions 间的负载均衡,也就是说分配的随机前缀的种类数量应该和你想把数据分散到的那些 region 的数量一致。只有这样,加盐之后的 rowkey 才会根据随机生成的前缀分散到各个 region 中,避免了热点现象。

缺点:

大白话来理解就是加了盐就尝不到原有的味道了。因为添加的是随机数,添加后如果还基于原 RowKey 查询,就无法知道随机数是什么,那样在查询的时候就需要去各个可能的 Region 中查找,同时加盐对于读取是利空的。并且加盐这种方式增加了读写时的吞吐量。

3.2.3 哈希(Hashing)

最后介绍大家最熟悉的哈希方法,不管是学的啥技术,都会涉及到哈希,也都大同小异,比较简单。

这里的哈希是基于 RowKey 的完整或部分数据进行 Hash,而后将哈希后的值完整替换或部分替换原 RowKey 的前缀部分。这里说的hash常用的有MD5、sha1、sha256 或 sha512 等算法。

适用场景:

其实哈希和加盐的适用场景类似,但是由于加盐方法的前缀是随机数,用原 rowkey 查询时不方便,因此出现了哈希方法,由于哈希是使用各种常见的算法来计算出的前缀,因此哈希既可以使负载分散到整个集群,又可以轻松读取数据。

缺点:

与反转类似,哈希也打乱了 RowKey 的自然顺序,因此也不利于 Scan。

4.RowKey设计原则

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

RowKey 在字段的选择上,需要遵循的最基本原则是唯一原则,因为 RowKey 必须能够唯一的识别一行数据。无论应用的负载特点是什么样,RowKey 字段都应该首先考虑最高频的查询场景。数据库通常都是以如何高效的读取和消费数据为目的,而不仅仅是数据存储本身。然后再结合具体的负载特点,再对选取的 RowKey 字段值进行改造,结合 RowKey 的优化,也就是避免热点现象的那些方法来优化就可以了。

4.1 长度原则

RowKey 本质上是一个二进制码的流,可以是任意字符串,最大长度为64kb,实际应用中一般为10-100byte,以byte[]数组形式保存,一般设计成定长。官方建议越短越好,不要超过16个字节,原因可以概括为如下几点:

  1. 影响HFile的存储效率:HBase 里的数据在持久化文件HFile中其实是按照 Key-Value 对形式存储的。这时候如果 RowKey 很长,比如达到了200byte,那么仅仅 1000w 行的记录,只考虑 RowKey 就需占用近 2GB 的空间,极大的影响了 HFile 的存储效率。
  2. 降低检索效率:由于 MemStore 会缓存部分数据到内存中,如果 RowKey 比较长,就会导致内存的有效利用率降低,也就不能缓存更多的数据,从而降低检索效率。
  3. 16字节是64位操作系统的最佳选择:64位系统,内存8字节对齐,控制在16字节,8字节的整数倍利用了操作系统的最佳特性。

4.2 唯一原则

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

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

4.3 排序原则

HBase 会把 RowKey 按照 ASCII 进行自然有序排序,所以反过来我们在设计 RowKey 的时候可以根据这个特点来设计完美的RowKey,好好的利用这个特性就是排序原则。

4.4 散列原则

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

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

结合前面分析的热点现象的起因,思考:

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

HBase 里的 RowKey 是按照字典序存储,因此在设计 RowKey 时,咱们要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。如果最近写入 HBase 表中的数据是最可能被访问的,可以考虑将时间戳作为 row key 的一部分,由于是字典序排序,所以可以使用Long.MAX_VALUE - timestamp作为 row key,这样能保证新写入的数据在读取时可以被快速找到。

总结

看到这里 RowKey 的各个方面应该都已经搞懂了,本文从 RowKey 的原理,可能出现的问题,如何优化及各个优化措施对应的缺点和适用的场景,设计原则等角度对 RowKey 进行了详细全面的解析,相信一定能对你有所帮助。