本文是对《Hadoop系列四——HBase简介》一文的补充,不过本文不会进行系统性介绍,只是针对一个个独立的点介绍,并且会不断更新,有点类似于FAQ吧。

1. HBase的架构图

简易版架构图:

简易版架构图

复杂版架构图:

复杂版架构图

架构讲解见《Hadoop系列四——HBase简介》。

2. HBase的数据模型

在《Hadoop系列四——HBase简介》里面已经介绍过HBase的数据模型了,但这个的确非常重要,这里再做一些补充。HBase是根据Google的Bigtable论文实现的开源版"Bigtable",所以对Bigtable的描述同样适用于HBase:

A Bigtable is a sparse, distributed, persistent multi-dimensional sorted map. The map is indexed by a row key, column key, and a timestamp; each value in the map is an uninterpreted array of bytes.

这里有几个关键词我已经加粗了,从这几个关键词可以引出来多个问题。

2.1 HBase里面的"NULL值"处理

第一个就是sparse关键字。首先NULL值是RDBMS里面的概念,HBase里面其实没有这个概念,这里只是为了作对比,介绍稀疏性这个特性。我们把HBase想成一个二维矩阵(这样不准确,但有助于理解),那么其实就是一个稀疏矩阵。在RDBMS里面,没有值的地方一般用NULL表示,虽然相比于其他类型,NULL占很小的空间(一般是1bit),但仍然是占空间的,在RDBMS里面这没有问题,主要有两个原因:(1)RDBMS里面数据量不会很大,目前RDBMS能承载的最大数据量一般也就是百万级别的。(2)如果把RDBMS也看成二维矩阵的话,一般RDBMS都属于稠密矩阵,所以NULL值不会占大多数。

但HBase不一样,它正好与RDBMS的特性相反:数据量很大,一般都大于百万级别(如果小于这个量级,那你该重新考虑是否有必要使用HBase了);NULL值可能占很大部分,这个和HBase的设计使用有关系。比如一个表里面有1百万个列,有1千万行数据,每一行里面可能只有一小部分列有值,其他列都是“NULL值”(实质是没有值)。所以,在HBase里面,"NULL值"是不存储的,即不占用任何存储。其实这个和稀疏矩阵的存储原理上是一样的,只存储有数据的cell的“坐标”及值,其它的默认就是NULL。当我们在HBase里面获取一行数据的时候,只会返回有数据的列,不像RDBMS会返回所有列,没有值的用NULL占位。

更多信息见本文“HBase的逻辑存储和物理存储”部分。

2.2 分布式

想想如果因为性能或存储等不够了,需要将RDBMS扩展成分布式的,能否做到?也许可以做到,但一般是受限的,而且难度比较高,比如像业内的GreenPlum,如果还想要保证RDBMS的所有特性,那就只能用更好的硬件做垂直扩展了。但HBase本身就是分布式的,扩展一个HBase节点基本上没有什么难度。所以这也是很多企业选择HBase的一个非常重要的考量。

2.3 多维有序map

多维的概念在我的《Hadoop系列四——HBase简介》里面已经讲过了。

有序之前也提过,在HBase里面,不论是内部存储,还是查询返回的数据,都是有序的:依次按照row key、Column Family、Column qualifier、Timestamp四个维度排序,时间戳是按照从新到旧排的,其它都是字典序。

2.4 HBase里面的数据是否有类型

上面对于value的描述是“uninterpreted array of bytes”,也就是没有类型,默认按字节流解析(我们知道任何类型其实都可以按字节流其解析)。其实在HBase里面除了会被作为路径的名称之外,其它都是作为字节流的,也就是没有类型。具体就是:表名、Column Family会作为路径名,所以名称必须是合法的路径名,也就是必须是可打印字符;row key,Column qualifier、Cell都是没有类型的,也就是可以是任何数据。实际中,我们经常会在row key、Column qualifier中存储数据,而不光是cell里面。

3. HBase的逻辑存储和物理存储

引用HBase:The Definitive Guide上的一幅图:

HBase存储图

第一幅图(左上角)是HBase里面一个典型的表的逻辑图,里面有cf1cf2两个Column Family(以下简称CF),每个CF里面有两列。里面的红色和黄色小块表示有数据,其他地方都没有数据(如前面所说,是一个稀疏矩阵),多个层叠的部分表示有多个版本的数据。

第二幅图(右上角)也是一个逻辑图,主要为了说明以下几个点:

  • 不同CF里面的数据是分开存储的;
  • 同一个CF里面的数据是按照row key顺序存储的;
  • 统一cell里面有多个版本的数据的时候,新版本数据在前,旧版本数据在后,这样方便先取到新版本数据。

第三幅图(右下角)是上面的逻辑存储在物理文件上的存储形式,需要注意以下几个点:

  • 不同CF的数据是存储在不同的文件里面的(Storefile或HFile),这就是为什么我们要将要一起使用的数据字段定义在同一个Column Family的原因;
  • 同一个文件里面还是按照row key、CF、Column qualifier排序的;
  • 每一条数据里面都要存储Key(row key、Column Family、Column qualifier、Timestamp)和value。在RDBMS里面我们设计字段名时一般要求能够“见名知意”,但在HBase里面不推荐这样做,Key的设计在保证功能的前提下,越短越好(比如仅用一个字母表示),至于其含义可以其它地方记录,比如文档里面。

第四幅图(左下角)是为了说明查询时指定各个Key对性能的影响:

  • 指定row key可以大幅度提高查询性能,因为根据row key可以确定在哪些region上面查(也就是说可以跳过那些不包含该row key的region)。在scan命令里面,可以通过STARTROWSTOPROW指定row key范围。
  • 指定Column Family可以大幅度提高查询性能,因为根据CF可以确定跳过哪些Storefile/HFile,一般查询时都建议指定CF。
  • 指定Timestamp也可以较大幅度提高查询性能,因为每个Storefile会存储它所保存的所有数据的时间区间,如果所指定的Timestamp不在该区间内,则直接跳过。
  • 指定Column QualifierValue的过滤条件可以提高查询性能,但提高的很少。因为必须把每个Cell的值读出来和指定的条件做对比。

4. Tall-Narrow or Flat-Wide表

Tall-Narrow也就是我们所说的“窄表”,Flat-Wide是“宽表”。举个例子,比如我们要存储一个用户的邮件信息:用户ID、邮件ID、邮件内容。如果按照Tall-Narrow的思想去设计,表结构可能是下面这样:

# 将用户id和邮件id拼接成row key,邮件内容作为一列数据
userid-emailid, cf:emailbaody

如果按照Tall-Narrow思路去设计,表结构可能是下面这样:

# userid为row key,emailid和emailbody作为两个列
userid, cf:emailid, cf:emailbody

两种设计各有利弊,使用宽表的好处主要在于HBase的ACID特性仅限于行内,所以如果把所有数据都放在一行,那可以很好的利用其ACID特性。而窄表在实际使用中更加常见一些,因为HBase里面的的row key类似于RDBMS里面的主键,所以我们尽可能将要经常查询的维度放在row key里面,可以提高查询性能;另外一个表里面不推荐有太多的Column Family,一般1个最好,最多也不要超过3个,具体见本文“Column Family的数量”部分。

5. HBase的表和Column Family能不能修改

先说结论:可以修改。使用alter命令可以修改表和Column Family,具体语法可以help "alter"查看。这里需要注意两点:

  1. 0.92版本之前,表必须先disable后才可以修改。之后的版本增加了一个配置项hbase.online.schema.update.enable,如果设为true,那可以直接修改,不需要disable。但官方推荐生产环境最好还是先disable再修改,在线修改可能会引发一些问题。
  2. 修改动作并非立即生效,而是等待下一次major compaction,Storefile重写之后才会生效。

6. HBase的Compaction

我们知道当MemStore里面的数据量达到一定值的时候,就会落盘形成StoreFile(HFile),这样就会形成很多文件,而Compaction就是将这些文件合并成大文件。HBase里面有两种Compaction:Minor compactionsMajor compactions,主要有如下区别:

  • Minor compactions一次只选取少量存储在一起的文件做合并压缩,其结果就是一个store(或Column Family)的数据被合并成了多个大一些的Storefile,而Major compactions合并之后的结果是一个store(或Column Family)的数据全部到一个Storefile里面去了。
  • Minor compactions合并时不删除已经标记删除或者过期版本的数据,而Major compactions会删掉那些标记删除或过期版本的数据。所以需要注意,HBase里面的删除是标记删除,真正的物理删除发生在Major compactions阶段。

虽然Compaction合并文件是为了提高性能,但合并这个操作却是消耗资源的,就跟Jvm的GC一样。默认Major compactions一周一次。

7. Column Family的数量

先说结论:一个表内的Column Family最好1个,最多不要超过3个。原因主要有两点:

  1. 现在HBase的flush(MemStore满了之后就会flush)和Compaction操作都是基于region的,从前面的架构图中可以看到一个region里面是包含多个Column Family的,所以当region内的某个Column Family需要flush或Compaction的时候,和它处于同一region内的其它Column Family也会一起flush或Compaction,但它们可能只有少量新增数据,这就会浪费IO。
  2. 假设有两个Column Family:CF-A和CF-B,其中CF-A有一百万条数据,CF-B有一千万条数据。假设CF-A占了100个HFile,CF-B占了1000个HFile,因为数据写入的先后顺序,很可能CF-A的100个文件会被CF-B的1000个文件打散,本来可能一个RegionServer上面的region足够存储所有的CF-A的HFile了,现在可能被打散到多个RegionServer上面去了,这样查询CF-A的数据的时候效率就会降低。这种现象称为“Cardinality of ColumnFamilies”。

8. Value的版本数和TTL

版本数指我们之前说的Timestamp,和这个特性相关的设置有两个:max versionsmin versions,其含义也很明确。max versions的默认值是1,min versions的默认值是0,表示不启用多版本这个特性,即往一个Cell里面重复写数据会覆盖,而不是保留多个版本。

min versions和HBase的TTL(time-to-live)一起使用。我们可以给Column Family设置一个TTL时间(单位为秒),时间到期后的row会自动被删除。如果一个Storefile里面的row全部是过期的,那么在minor compaction阶段这个Storefile会被删除,我们可以通过把hbase.store.delete.expired.storefile设置为false或者把min versions设置为非0值来关闭删除这个特性。

9. 选择HBase还是RDBMS

一般来说,这个选择是比较好做的。HBase、Impala等数据库的诞生并不是为了替代传统的RDBMS,只是为了解决新的RDBMS解决不了或者解决起来比较困难的问题。所以,如果你的数据量并不大(一般以百万为分界线),那一般应该优先选择RDBMS,毕竟RDBMS支持完善的ACID,SQL,多级索引,各种Join,完善的类型等丰富的特性,这些都是HBase所不具备的。

但如果你的数据量非常大,RDBMS已经无法支撑了,那就可以考虑HBase等分布式数据库了。但如果你的业务离不开RDBMS的一些特性(比如各种Join、SQL、完善的ACID等),那可能就需要考虑类似于GreenPlum这种MPP数据库了。

10. FAQ

  • HBase是否支持ACID?
HBase里面只支持受限的ACID(Atomicity, Consistency, Isolation, and Durability):仅支持行内的ACID,跨行不支持。也就是对于同一行的操作是可以保证ACID的,但是多行操作是不行的。更多信息可参考:ACID in HBase
  • HBase和Hive如何选择?
这两个不是一个层级的东西,没有可比性。如果你还在这二者之间纠结,那可能你对它们有些误会,或者还不清楚你自己的需求。非要比的话,那就是HBase一般做实时查询;而Hive一般作为离线数据仓库,Hive后面是MapReduce/Spark,所以无法做到实时。
  • HBase支不支持join?
先说结论:不支持。HBase读取数据时支持GetScanGet的后台实现是Scan的一种特殊情况而已)操作,RDBMS里面的join在HBase里面是不支持的,但我们可以在表设计上支持一定程度上的"join"操作,比如将需要join的字段拼接起来作为row key
  • HBase支持SQL吗?

    HBase不支持SQL,只提供了各种API。但Apache下有个Phoenix项目,通过该项目可以使用SQL语句操作HBase。
  • HBase的一个region多大合适?
注意,这里说的是region,不是RegionServer。一个region保持在10~50GB比较好。
  • HBase一个表包含多少个region比较好?
一般一个表包含50~100个region和1~2个Column Family比较好。
  • HBase的Cell里面存储的Value有大小限制吗?
没有,但一般不要超过10MB(对于MOB对象不要超过50MB)。如果超过了这个大小,可以将对象存到HDFS上面,然后再HBase里面存储HDFS路径。
  • HBase里面的daughter是什么?
HBase的region分裂的时候,分裂出来的两个新region称为"daughter",原来的称为"parent"。

11. Row key设计

Last but not the least,row key的设计是HBase表设计里面最重要的部分,没有之一。Row key设计不合理很容易会引起热点问题(hotspotting)——大量的客户端请求到达同一个节点(RegionServer)或少数一些节点,导致该节点负载急剧上升,性能下降,甚至不可用。这个问题容易出现的原因和HBase自身的设计有关系——row key是按照字典序排列的。当然这个设计是没有问题的,主要是为了快速访问。这里介绍一些常用的避免热点问题的方法。

11.1 Salt

第一种方式就是加盐值,这里的盐值不同于密码学里面的盐值。实质就是事先定几个随机的前缀,然后给每一个row key加上一个前缀,前缀的选择是随机的。如果你想让数据分布在N个region上,一般就有N个前缀。比如现在我们有如下形式的row key:

foo0001
foo0002
foo0003
foo0004
...

显然这样的row key在HBase里面会连续存储在一起,这样在写的时候就会出现热点问题。此时如果使用加盐值的方式解决的话,我们可以这样操作:假设我们想让数据分布到不同的四个region上,那我们可以选取四个前缀,比如a~d四个字母,这样我们新的row key就变成下面这样了:

a-foo0001
b-foo0002
c-foo0003
d-foo0004
...

这样就将数据打散了。由于这种前缀的选择是随机的,所以相同的row key可能拼接了不同的前缀:

b-foo0001
c-foo0002
a-foo0003
d-foo0004
...

所以这种方式提高了写的吐吞量,但读的时候就会比较麻烦。

11.2 Hash

Hash其实是对Salt的一种优化。在Salt里面,前缀的选择是随机的,这样相同的row key可能加了不同的前缀。而Hash就是在row key和前缀之间建立一种哈希,使得相同的row key总是加相同的前缀,其它和Salt相同。

11.3 Key逆序

这种应该是现在使用比较普遍的一种解决row key热点的技术,对于固定长度(比如卡号、时间戳等)或者固定结构(比如域名)等非常好用。但其缺点就是牺牲了原来key的顺序。

Row key的设计和读写模式、业务、数据都有很大关系,所以不可一概而论,介绍的几种方法没有哪个是普适的,也没有哪个是完美的,具体问题还需具体分析。上面介绍的都是一些理论,关于row key的设计实战的话,这里强力推荐一篇文章:Introduction to hbase Schema Design by AMANDEEP KHURANA)。篇幅稍微有点多,有兴趣的可以看一下原文,这里列一下作者提到的设计表的时候要考虑的一些比较重要的点:

  1. What should the row key structure be and what should it contain?
  2. How many column families should the table have?
  3. What data goes into what column family?
  4. How many columns are in each column family?
  5. What should the column names be? Although column names don’t need to be defined on table creation, you need to know them when you write or read data .
  6. What information should go into the cells?
  7. How many versions should be stored for each cell?

设计的时候需要将HBase一些重要的特性考虑到:

  1. Indexing is only done based on the Key.
  2. Tables are stored sorted based on the row key. Each region in the table is respon-sible for a part of the row key space and is identified by the start and end row key.The region contains a sorted list of rows from the start key to the end key.
  3. Everything in HBase tables is stored as a byte[ ]. There are no types.
  4. Atomicity is guaranteed only at a row level. There is no atomicity guarantee across rows, which means that there are no multi-row transactions.
  5. Column families have to be defined up front at table creation time.
  6. Column qualifiers are dynamic and can be defined at write time. They are stored as byte[ ] so you can even put data in them.

不论是上面的7个问题,还是后面的6个特性,基本上在本文或者之前HBase的介绍中都有提到。其实,我个人认为对于这些开源系统的学习分两个方面:一方面学习这些系统如何使用,另一方面也可以学习一下它的设计,很多理念和方法在我们设计自己产品的时候也是可以借鉴的。

References

  1. Apache HBase Reference Guide(Version3.0.0-SNAPSHOT).
  2. HBase:The Definitive Guide.