一. 架构

1. 数据模型

1.1 基础概念

  • 表(table):列式存储,支持高表&宽表(上亿行,上百万列)
  • 行(row):每一行由唯一的行键确定
  • 列族(columnFamily):每一行包含一个或多个列族,是列的集合
  • 列(column):列式存储,列是最基本单位,可能有多个版本的值
  • 时间戳(Timestamp):列的不同版本之间用时间戳区分
  • 单元格(cell):列的每一个版本是一个单元格,是存储的基本单位

HBase最基本的单位是列(column),一列或者多列形成一行(row),若干行数据组成了一张表(table)。听起来是一个非常普通的列式存储数据库,但是,它和传统数据库有很大的不同。

1.2 与传统数据库的区别

a. HBase的每一行由唯一的行键确定

在某种程度上,行键相当于传统数据库的primary key,区别在于,primary key是可选的,而HBase的每张表都必然会有行键。除了行键之外,HBase表不能对列添加索引。HBase是一个形式的数据库,行键就是它的key

b. HBase引入了列族(columnFamily)的概念

  • HBase是一个列式存储的数据库,因此列的使用是非常灵活的,不必在表定义的时候就定好列名,但是必须在建表的时候定义好列族名字。
  • 一张HBase表存储的列的数量可以是无限的,但是列族的数量最好控制在2-3个(原因在备注中[1])
  • 列必须属于某个列族,不同列族之间可以有同名列
  • 列族的作用是,将那些数据量和属性相似的列聚集在一起,以便我们给这些列定义一些共同的存储方式属性(e.g. 数据压缩,保存到读缓存中)

c. HBase的列值可以有多版本

在HBase表中,行键、列族、列名和时间戳才能唯一确定一个值。每个值是一个单元格(cell),是存储的基本单位。每一行数据的每一列,都可以存储多个值,每个版本的值之间通过时间戳确定,在存储的时候,这些值也按照时间戳逆序排列,保证客户端永远读到最新的数据。但是,每个列族可以存储的最大版本数是确定的,并且是在建表的时候就定义好的。

另外,单元格的时间戳是可以由用户自行指定的,如果不指定,服务器就会将接收到写请求的服务器时间作为单元格的时间戳。通常情况下,最好不要自己指定时间戳,因为客户端总是难以保证,指定的时间戳是按照写顺序递增的。

d. 反范式化

HBase是一个NoSQL(Not-only-SQL)数据库,不提供复杂的查询方式,包括join。另外,相对于MySQL,HBase的可扩展性很好,存储资源要廉价很多。因此,在设计数据库的时候,我们总是倾向于反范式化,以方便后期的数据查询。

1.3 数据模型抽象

HBase实际上是按照谷歌的bigtable实现的,而谷歌在bigtable论文的开篇就介绍了bigtable的特点:A Bigtable is a sparse, distributed, persistent multidimensional sorted map。所以HBase在本质上,是一张有序的多维map,数据模型可以抽象成:

<rowKey : columnFamily : qualifier : timestamp, value>

这样的优点是,HBase只存储有值的单元格,对于一张稀疏表来说,可以节省很多存储空间;但是,为每个cell都存储了rowKey, columnFamily, qualifier,因此cf的名字不要太长。

2. 存储模型

HBASE功能原理简述_缓存

存储模型

2.1 存储概览

a. 数据分片存储

在HBase中,一张表的数据会被分成几份,每一份数据为一个region每个region内存储的key是连续范围内的,不同region存储的key范围不重合;这些region可能被存储在同一台机器上,也可能存储在不同的机器上。HBase作为一个分布式数据库,对数据进行分片,可以提升吞吐量。

b. HLog:Write-Ahead-Log,写操作先写日志

HLog的作用是,当一台regionServer crash了,可以利用HLog来恢复内存中未持久化到硬盘中的数据。需要注意的是,同一台server上的所有region共用一个HLog实例,因为假如每个region拥有一个独立的HLog,服务器会花费很多时间在磁盘寻道上。

c. MemStore:写缓存,每个store拥有独立的写缓存

在HBase中,所有的写操作全部写到内存中,当写缓存(MemStore)写满,再刷写(flush)到磁盘中[2],形成一个新的文件。这样做的目的,是为了高速响应那些写请求。

d. HFile:磁盘文件

在存储上,HBase完全依赖HDFS,磁盘操作是直接调用HDFS的API(HDFS在维持data locality这一点上足够智能)。另外,之前提过HBase定义列族的一个原因是为了方便存储,事实上,同一列族的数据会被写到同一文件,因为存储特性本来就是按照列族定义的。HBase的数据在底层文件中时以KeyValue键值对的形式存储的,HBase没有数据类型,HFile中存储的是字节,这些字节按字典序排列。

e. 读缓存:同一server上所有region共用

既然HBase有写缓存,相对应的应该有读缓存。与写缓存不同的是,HBase的读缓存是同一server上的所有region共用的。当HBase读取磁盘上某一条数据时,HBase会将整个HFile block[3]读到cache中[4]。因此,当client请求临近的数据时,因为临近数据已经被缓存到内存中,HBase的响应会更快,也就是说,HBase鼓励将那些相似的,会被一起查找的数据存放在一起。另外,当我们在做全表扫描时,为了不刷走读缓存中的热数据,千万记得关闭读缓存的功能。

2.2 行键的索引

a. 行数据查找步骤

  • hbase:meta表查找,获取数据所在的region id
  • 根据region id,到对应的region server上查找,在server上查找对应记录时,有三种方式

(1) 扫描缓存

(2) 块索引

(3) 布隆过滤器

b. rowKey索引:hbase:meta表

client会首先获取hbase:meta表的位置,再到对应的region server上读取这张表的内容(hbase:meta表其实就是一张HBase表)。读到这张hbase:meta表之后,client会缓存这张表,这张之后的查找就可以复用了。hbase:meta表的内容如下:

  1. | key | value |
  2. | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------|
  3. | | info:regioninfo (serialized HRegionInfo instance for this region) |
  4. | Region key of the format ([table],[region start key],[region id]) | info:server (server:port of the RegionServer containing this region) |
  5. | | info:serverstartcode (start-time of the RegionServer process containing this region)|

因此,通过查找hbase:meta表,client可以得知对应的数据存储在哪台server的那个region上,接下来就要到对应的server上查找相关数据了。

c. region server上的数据查找

当接收到一个读请求,server会初始化一个scanner查找内存中是否有相关数据;一个scanner查找硬盘文件中是否存储了相关数据。查找硬盘文件是一件相当繁重的体力活,为了加快文件查找,HBase借助了两个工具:块索引和布隆过滤器。

HBASE功能原理简述_缓存_02

块索引和布隆过滤器

d. rowKey索引:块索引

块索引存储在HFile文件中的末端,当HBase在查找文件中是否保存了目标数据时,首先会将块索引读入内存。因为HFile中的KeyValue字节数据,是按照字典序排列的,而块索引存储了文件中所有块(HFile block)的起始key,因此可以根据块索引迅速定位需要查找的块,只将可能保存了目标数据的块读到内存中,能加快查找速度。

e. rowKey索引:布隆过滤器

虽然块索引帮助减少了需要读到内存中的数据,我们依然需要查找每个文件中的一个块,才能完成磁盘数据查找,而布隆过滤器则可以帮助我们跳过那些显然不包含目标数据的文件。因为布隆过滤器的特点是,能迅速判断一个数据集合中包不包含目标数据,判断结果有两种,不包含和可能包含。如下图所示,布隆过滤器能帮助跳过一些肯定不包含目标数据的文件。

HBASE功能原理简述_缓存_03

布隆过滤器

和块索引一样,布隆过滤器也被存储在文件末端,会被优先加载到内存中。另外,布隆过滤器分行式和列式两种,列式需要更多的存储空间,因此如果是按行读取数据,没必要使用列式的布隆过滤器。布隆过滤器和块索引的对比如下:

块索引

布隆过滤器

快速定位记录在文件中可能的位置

快速判断文件中是否包含相应记录

2.3 将随机写转化成顺序写

HBase的存储是完全基于HDFS的,而HDFS的特点是不能对磁盘文件进行随机修改。因此,HBase无法对已写入磁盘文件的表记录进行随机修改,但是对于数据库来说,支持对表记录进行随机修改是基本功能。为此,HBase的方法是将随机写的操作转化成顺序写

首先,随机的写操作转化为文件追加操作,按照时间顺序排列,client读数据时总是优先读到最新的修改。而删除操作则转化为写入一个tombstone标记,标记着早于这个tombstone时间戳的对应行所有记录作废。

显然,因为HBase总是进行文件追加,随着时间积累,文件膨胀很快。major compact的一个作用就是,真正删除所有无效的过时数据。

2.4 HBase Compaction和Region Split

a. HBase Compaction

前面已经提到过,HBase数据写入的时候,总是先写入到写缓存(MemStore)中,当写缓存写满,则flush到磁盘形成一个新的磁盘文件。可以想象的是,随着时间增长,磁盘上这样的小文件会越来越多,HBase查找数据也需要越来越长的时间。为了避免这样的问题,HBase会做compaction,合并HFile文件,减少每次查找数据的磁盘寻道时间。compaction分为major compact和minor compact两种:

  • Minor compact:将多个小文件简单合并成一个大文件
  • Major compact:将同一列族的所有文件合并成一个大文件,并且删除过期无效的数据和tombstone标记[5]

b. Region Split

client不断向HBase写入数据,region管理的数据量不断膨胀。当一个region内存储的数据量到达阈值,则会触发HBase的region split操作,将老的region拆分成两个新的子region拆分的原则是数据量对半分。为了避免region拆分导致的IO瞬时上升,region拆分并不会立刻将拆分重写所有的磁盘文件文件,而是为每个子region创建reference文件,这些文件指向了旧的磁盘文件中对应记录的起始和终止位置。等到子region的compact操作被触发,在重写文件的时候,HBase才会为每个子region生成独立的磁盘文件。

3. 物理模型

HBASE功能原理简述_数据_04

HBase的架构是一个典型的master-slave模型,HBase的master节点叫HMaster,slave节点就是RegionServer。

3.1 Master的职责

处理集群相关的请求(来自client或者其他server节点)

  • 建表或者表变更的操作
  • 打开或者关闭一个region
  • metadata元数据的管理

集群监控(依赖Zookeeper)

  • 监控regionServer的状态以及负载均衡等
  • 跟踪hbase:meta表的位置
  • 后备master节点需要监控当前master节点是否活跃

3.2 RegionServer的职责

集群初始化过程中

当一个HBase集群起来之后,HMaster会在对应的regionServer上起一个HRegionServer进程。HRegionServer负责打开对应的region,并创建对应的HRegion实例。当HRegion打开之后,它会为每个表的HColumnFamily创建一个Store实例,ColumnFamily是用户在创建表时定义好的,ColumnFamily在每个region中和Store实例一一对应。每个Store实例包含一个或者多个StoreFile实例,StoreFile是对实际存储数据文件HFile的轻量级封装。每个Store对应一个MemStore。一个HRegionServer共享一个HLog实例。

集群运行过程中

  • compaction和split是由RegionServer独立判断决定是否执行的,但过程中包含一些必要的和master、ZK的通信
  • client端发起的读写请求,也直接由对应RegionServer处理(master不处理)[6],流程如下:

(1) client端向ZK请求hbase:meta表位置,取得表内容

(2) 查询meta表得知数据存在哪台RegionServer上

(3)直接与RegionServer通信,进行读写操作

注:

[1] 同一张表的column family数量不能超过2-3个。因为目前,flush和compaction操作是基于region进行的,当一个column family触发了MemStore flush操作,相邻的column family都会被刷写到磁盘,即使它们MemStore内的数据量还很小。因此,如果同一张表内column family的数量过多,flush和compaction将会带来更多不必要的I/O负载(当然这个问题可以通过,将flush和compaction改成列族之间互不影响来解决)。通常情况下,定义表的时候尽量使用单列族,除非列与列的查询是相对独立的,再考虑使用多个列族,比如client并不会同时请求两个列族的数据。

当同一张内有多个列族时,注意一些列族间的数据量是否一致,假如列族A和列族B的数据量相差悬殊,列族A的大数据量会导致表数据被分片到很多个机器上,此时再对列族B的数据做扫描,效率会很低。

[2] 能触发MemStore flush操作的有三种情形:

  • 当一个MemStore的数据量达到hbase.hregion.memstore.flush.size,同一region的所有MemStore的数据都会被刷写道磁盘
  • 当全部的MemStore的数据量达到hbase.regionserver.global.memstore.upperLimit,同一RegionServer的多个region的MemStore的数据会被刷写到磁盘。按照每region的MemStore大小,从大到小刷写到磁盘,直到总的MemStore大小下降到hbase.regionserver.global.memstore.upperLimit
  • 当region server的WAL的log数量达到hbase.regionserver.max.logs,该server上多个region的MemStore会被刷写到磁盘(按照时间顺序),以降低WAL的大小。

[3] HFile block:HFile块和Hadoop块是两个独立的概念。HFile块的默认大小是64KB,而Hadoop块的默认大小为64MB。另外,如果有需要的话,用户还可以自行定义HFile块大小。一般情况下,如果客户端都是顺序访问表记录,在读缓存的作用下,建议使用较大的HFile块;如果客户端都是随机访问表记录,建议使用较小的HFile块,不过也需要更多的内存来存储块索引(块索引会优先存放在cache中),并且创建过程也会变得更慢,因为我们必须在每个数据块结束的时候刷写压缩流,导致一个FS I/O刷写。

[4] 关于读缓存的更详细资料:http://zh.hortonworks.com/blog/hbase-blockcache-101/

[5] HBase默认每7天对HBase做一轮major compact,在0.96的版本之前,这个周期是1天。

[6] 正因为client读写数据的过程没有master节点的参与,如果master failover了,hbase集群仍然可以稳定运行一段时间,只是像region分裂,RegionServer failover处理等需要master节点参与的工作,无法完成了。