大白话彻底搞懂HBase Rowkey设计和实现

172 阅读10分钟

这是云祁的第 55 ****篇文章

一、前言

大家好,我是云祁!

周末面试了一位小伙伴,看他简历上写着掌握HBase存储原理以及RowKey设计,于是忍不住多问了几句 ???? 谁让我最近在肝HBase......

大家都知道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时,需要充分利用排序存储这个特性,将经常一起读取的行存储放到一起,要避免做全表扫描,因为效率特别低。

三、什么是数据热点?

3.1 热点现象产生

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

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

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

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

3.2 避免数据热点的方法

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

3.2.1 反转(Reversing)

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

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

3.2.2 加盐(Salting)

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

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

3.2.3 哈希(Hashing)

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

四、RowKey的设计原则

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

4.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分配的大量内存。

4.2 唯一原则

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

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

4.3 排序原则

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

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

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

4.4 散列原则

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

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

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

五、举个栗子

实际业务中,有一部分是用户在日历上记录自己的行为。需要储存在RowKey中的维度有:用户ID(uuid,不会超过十亿)、日历上的日期(date,yyyyMMdd格式)、记录行为的类型(type,0~99之间)。记录的详细数据则存储在列f:data中。根据查询逻辑,我们可以设计的RowKey格式如下:

9~79809782~05~0008839540

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

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

参考

我是「云祁」,一枚热爱技术、会写诗的大数据开发猿,欢迎大家关注呀!

往期推荐

全网最通俗易懂的HBase架构及原理

HBase入门为什么可以这么简单?

读书笔记 | 《大数据之路:阿里巴巴大数据实践》,DT时代的经验之谈!

我的2020年终回顾:人生,海海,破浪前行

????分享、点赞、在看,给个三连击呗!????