Google划时代的三篇大数据论文分别是2003年的Google File System、2004年的Google MapReduce、2006年的Google Bigtable,Doug Cutting和一些优秀的前辈根据这三篇论文开发了开源的HDFS,MapReduce,HBase。今天记录的是HBase,它是Google的Bigtable的开源实现,是一个分布式的结构化数据存储系统,用来处理海量数据,通常是PB级的数据。
本文主要参考以下几篇文章:
HBase原理与实践 - 胡争 范欣欣
深入理解 Hbase 架构(翻译) - navi 这篇非常好!本文还参考了:
我终于看懂了HBase,太不容易了… - Java3y的文章 - 知乎
HBase架构介绍 - NYC
HBase原理–RegionServer核心组件之MemStore - 小明的数据脚印
主要内容
- HBase概述和使用场景
- HBase整体架构
- HBase存储
- HBase读写流程
- Compaction,高可用性和灾备
HBase概述
HBase是基于Hadoop构建的一个高可用、高性能的分布式NoSQL数据库,通过在廉价服务器上搭建大规模结构化存储集群,提供海量数据高性能的随机读写能力。HBase也是KV数据库,但是和Redis不同,HBase是分布式持久化的存储。
HBase是Hadoop生态的一部分,依赖HDFS的存储。

对于一个分布式数据库,最大的特点就是扩展性强,成本较低,可用性高。当一个Mysql的单表数据量暴涨的时候,不得不用分库分表来拆分数据,而分布式数据则只需要横向扩展机器就好了(当然也不止如此)。
对于每一个流行的数据库,都有其适用的场景,在知乎上有这样一个问题 HBase为什么火?它适用于那些业务场景? ,讨论中提到了下面三个主要的场景:
- 写密集型,读相对没有那么多的应用,这是因为HBase采用了LSM的设计,牺牲了少部分的读性能达到更好的写性能。比如历史数据,日志等。
- 大数据量的查询,不适合大范围的查询和复杂查询,这是因为KV的原因。
- 对性能和可靠性要求非常高的应用。
- 数据量较大,而且增长量无法预估的应用,HBase的横向拓展能力很好,加机器比较容易。

HBase适用于上面几个场景,是其数据模型,整体架构,存储设计,数据结构决定的。曾经在某个MeetUp或者知乎某个问答上看到过有人说,了解了一个数据库的数据结构就能了解它的使用场景,虽然不准确,但是可以进行一个大致判断。HBase的数据结构大致为,列式,LSM树,跳表,KeyValue存储,布隆过滤器。这些经典的数据结构在很多数据库中都有应用,后面有空了会再详细记录。
HBase整体架构

HMaster
HMaster负责 Region 的分配,DDL(创建,删除表)等操作,每台 Region Server 都会与 Master 进行通信,HMaster 的主要任务就是告诉 Region Server 它需要维护哪些 Region,具体功能如下:
分配 Region 给 Region Server,并检测 Region Server 的新增和过期;
管理 Region Server 的负载均衡,动态调整 Region 分布;
在 Region 分裂后,负责新的 Region 的分配;
在 Region Server 停机后,负责失效 Region Server 上的 Region 的迁移;
对于 GFS 上的数据进行垃圾回收(GC);
管理表(Table)和列族的 Schema 变更(注意存还是在zk上),比如表和列族的创建与删除。
HMaster监控集群中所有 Region Server 实例是从 Zookeeper 获取通知信息来实现的。

HRegionServer
Region Server:由多个Region 组成,在整个集群中可能存在多个节点,每个节点只能运行一个 Region Server,负责对 HDFS 中读写数据和管理 Region 和 HLog,负责切分在运行中变得大的 Region。
HLog:Write ahead log(WAL),到达 Region 上的写操作首先被追加到 HLog 中, 然后才被加载到MemStore,主要功能为故障修复,当某台 Region Server 发生故障, 新的 Region Server 在加载 Region 的时候可以通过 HLog 对数据进行恢复。
Region:HBase 表(Table)根据 rowkey 的范围被水平拆分成若干个 region。每个 region 都包含了这个region 的 start key 和 end key 之间的所有行(row)。Regions 被分配给集群中的某些节点来管理,即 Region Server,由它们来负责处理数据的读写请求。每个 Region Server 大约可以管理 1000 个 regions。Region由多个Store组成,HBase使用表存储数据集,当表的大小超过设定的值时, HBase会自动将表划分为不同的Region,它是HBase集群上分布式存储和负载均衡的 最小单位。
Store:由两部分组成:MemStore 和 StoreFile。首先用户写入的数据存放到 MemStore 中,当MemStore满了后刷入StoreFile。
Region Server 中还有数据的缓存:
- BlockCache:这是读缓存,在内存中存储了最常访问的数据,是 LRU(Least Recently Used)缓存。
- MemStore:这是写缓存,在内存中存储了新的还未被持久化到硬盘的数据。当被写入硬盘时,数据会首先被排序。注意每个 Region 的每个 Column Family 都会有一个 MemStore。

Zookeeper
HBase深度依赖Zookeeper,它使用 Zookeeper 做分布式管理服务,来维护集群中所有服务的状态。Zookeeper 维护了哪些 servers 是健康可用的,并且在 server 故障时做出通知。Zookeeper 使用一致性协议来保证分布式状态的一致性,这需要三台或者五台机器来做一致性协议。
Zookeeper 存储的是 HBase 中的ROOT表(根数据表)和META表(元数据表), 元数据表保存普通用户表的Region标识符信息,标识符格式为:表名+开始主键+唯 一ID。 随着Region的分裂,标识符信息也会发生变化,分成多个Region后,需要由 一个根数据表来贯穿多个元数据表。
此外,ZooKeeper 还负责 Region Server 故障时,通知 HMaster 进行 Region 迁移,若 HMaster出现故障,ZooKeeper 负责恢复 HMaster,并且保证有且只有一个HMaster 正在运行。
当客户端访问 HBase 时,访问时首先访问 Zookeeper–ROOT–META-table。
底层存储
HBase的底层存储是基于 Hadoop HDFS 的:
- Hadoop DataNode 负责存储 Region Server 所管理的数据。所有的 HBase 数据都存储在 HDFS 文件中。Region Server 和 HDFS DataNode 往往是分布在一起的,这样 Region Server 就能够实现数据本地化(data locality,即将数据放在离需要者尽可能近的地方)。HBase 的数据在写的时候是本地的,但是当 region 被迁移的时候,数据就可能不再满足本地性了,直到完成 compaction,才能又恢复到本地。
- Hadoop NameNode 维护了所有 HDFS 物理 data block 的元信息。
HBase存储
下面这张图来自HBase:The Definitive Guide

第一幅图(左上角)是HBase里面一个典型的表的逻辑图,里面有 cf1 和 cf2 两个 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 命令里面,可以通过
STARTROW
和STOPROW
指定 row key 范围。 - 指定 Column Family 可以大幅度提高查询性能,因为根据CF可以确定跳过哪些Storefile/HFile,一般查询时都建议指定CF。
- 指定 Timestamp 也可以较大幅度提高查询性能,因为每个 Storefile 会存储它所保存的所有数据的时间区间,如果所指定的 Timestamp 不在该区间内,则直接跳过。
- 指定 Column Qualifier 和 Value 的过滤条件可以提高查询性能,但提高的很少。因为必须把每个 Cell 的值读出来和指定的条件做对比。
举个例子,比如下面这张表:

虽然在逻辑视图是完整的一张表,但实际的存储并不是连续的,根据 row key 的不同被划分为了不同的Region,存储时按照row key字典序排序存储。不同的Region可能存储在不同的物理机上。
行中的列也被分为不同的“列族”,即 Column family。同一个列族的所有成员具有相同的列族前缀。一般的,存放在同一列族下的数据通常都属于同一类型(可以把同一个列族下的数据压缩在一起) 一个表的列族必须在创建表时预先定义。Column family 的数据是一起存放的,所以与其说 HBase 是列式存储,不如说是列族存储。
HBase 还允许数据有不同的时间戳,不同版本的数据按照时间戳倒序排序,即最新的数据排在最前面。
从物理视图的角度来看 Hbase 的key-value 存储是这样的:

在 HBase 中,值作为一个单元保存在单元格中。 要定位一个单元,需要满足“行键+列键+时间戳”三个要素。 每个Cell保存着同一份数据的多个版本。 Cell中没有数据类型,完全是字节存储。
逻辑视图 vs 物理视图

不同列族的数据被存在了不同的 Store 中,StoreFile 以 HFile 格式保存在 HDFS 上。HFile 是 Hadoop 的二进制格式文件。实际上 StoreFile 就是对 HFile 做了轻量级包装,即 StoreFile 底层就是 HFile,HFile 以有序 KeyValue 的形式存储,HFile 的具体数据格式如下:

HFile 使用多层索引来查询数据而不必读取整个文件,这种多层索引类似于一个 B+ tree:
- KeyValues 有序存储。
- rowkey 指向 index,而 index 则指向了具体的 data block,以 64 KB 为单位。
- 每个 block 都有它的叶索引。
- 每个 block 的最后一个 key 都被存储在中间层索引。
- 索引根节点指向中间层索引。
trailer 指向原信息数据块,它是在数据持久化为 HFile 时被写在 HFile 文件尾部。trailer 还包含例如布隆过滤器和时间范围等信息。布隆过滤器用来跳过那些不包含指定 rowkey 的文件,时间范围信息则是根据时间来过滤,跳过那些不在请求的时间范围之内的文件。
HBase读写流程
读流程
有一个特殊的 HBase Catalog 表叫 Meta table(它其实是一张特殊的 HBase 表),包含了集群中所有 regions 的位置信息。Zookeeper 保存了这个 Meta table 的位置。
当 HBase 第一次读或者写操作到来时:
- 客户端从 Zookeeper 那里获取是哪一台 Region Server 负责管理 Meta table。
- 客户端会查询那台管理 Meta table 的 Region Server,进而获知是哪一台 Region Server 负责管理本次数据请求所需要的 rowkey。客户端会缓存这个信息,以及 Meta table 的位置信息本身。
- 然后客户端会去访问那台 Region Server,获取数据。
在徐文浩的大数据经典论文专栏的Bigtable篇中提到,要经过3次IO才能获得数据具体存放的位置,最后再来一次请求查数据,专栏里面是还有一次请求 Root Tablet 位置,通过查 Root Tablet 再获得 Meta table 位置。并且因为客户端会缓存,其实不会有那么多次的IO。
对于以后的的读请求,客户端从可以缓存中直接获取 Meta table 的位置信息(在哪一台 Region Server 上),以及之前访问过的 rowkey 的位置信息(哪一台 Region Server 上),除非因为 Region 被迁移了导致缓存失效。这时客户端会重复上面的步骤,重新获取相关位置信息并更新缓存。

写流程
- Client 的 Put 操作会将数据先写入 WAL。
- 当数据写入WAL,然后将数据拷贝到 MemStore。 MemStore 是内存空间,数据并未写入磁盘。
- 一旦数据成功拷贝到 MemStore。 Client 将收到 ACK。
- 当 MemStore 中的数据达到阈值,数据会写入 HFile。
MemStore 在内存中缓存 HBase 的数据更新,以有序 KeyValues 的形式,这和 HFile 中的存储形式一样。每个 Column Family 都有一个 MemStore,所有的更新都以 Column Family 为单位进行排序。

在第二步的时候,因为只写入了内存,万一在还没写入到磁盘的时候断电了怎么办呢?因为 WAL 机制的存在,可以从操作日志从其他HRegionServer中回放这些操作恢复数据。
HBase Compaction(HBase的伸缩性)
Bigtable的设计目标是“可伸缩性”,为此放弃了一些别的特性,比如事务。而“可伸缩性”这个特点很大一部分就体现在Compaction这里。
HBase 的 MemStore 在满足阈值的情况下会将内存中的数据刷写成 HFile ,一个 MemStore 刷写就会形成一个 Hfile。随着时间的推移,同一个 Store 下的 HFile 会越来越多,文件太多会影响 HBase 查询性能,主要体现在查询数据的 io 次数增加。为了优化查询性能,HBase 会合并小的 HFile 以减少文件数量,这种合并HFile的操 作称为 Compaction,这也是为什么要进行 Compaction 的主要原因。
- 将多个小的 HFile 合并成一个更大的 HFile 以增加查询性能
- 在合并过程中对过期的数据(超过TTL,被删除,超过最大版本号)进行真正的删除,一般情况下的删除只是打了一个墓碑标记,因为HDFS不支持对文件的随机读写
在对HBase进行写操作的时候,进行Put和Update操作的时候,其实是新增了一条数据,即使是在进行Delete操作的时候,也是新增一条数据,只是这条数据没有value,类型为DELETE,这条数据叫做墓碑标记(Tobstone)。数据的真正删除是在compact操作时进行的。
Minor Compaction :会将邻近的若干个 HFile 合并,在合并过程中会清理 TTL 的 数据,但不会清理被删除的数据。
Major Compaction:会将一个 store 下的所有 HFile 进行合并,并且会清理掉过期的和被删除的数据,即在 Major Compaction 会删除全部需要删除的数据。值得 注意的是,一般情况下,Major Compaction 时间会持续比较长,整个过程会消耗 大量系统资源,对上层业务有比较大的影响。因此,生产环境下通常关闭自动触发 Major Compaction 功能,改为手动在业务低峰期触发。
读放大:每个 MemStore 可能会有多个 HFile,所以一次 read 请求可能需要多读个文件,这可能会影响性能,这被称为读放大。
写放大:Major Compaction 会重写所有的 HFile,会产生大量的硬盘 I/O 和网络开销。这被称为写放大。
如何决定哪些HFile需要Minor Compaction?
![图片来自 06HBase[完整版v1.0]-极客时间训练营,第 55 页](/2021/09/22/%E5%A4%A7%E6%95%B0%E6%8D%AE%E7%9A%84%E2%80%9C%E4%B8%89%E9%A9%BE%E9%A9%AC%E8%BD%A6%E2%80%9D%E4%B9%8B-HBase/图片来自 06HBase[完整版v1.0]-极客时间训练营,第 55 页.png)
首先内存中维护着一个filesToCompact(合并队列),在该队列中的Hfile将会被Minor合并。
当有新的HFile文件产生时,如果同一个列簇下的文件数大于等于hbase.hstore.compaction.min 时,就会将符合合并规则的文件放入合并队列,合并规则如下:
• 如果该文件小于hbase.hstore.compaction.min.size, 则一定会被添加到合并队列中。
• 如果该文件大于hbase.hstore.compaction.max.size,则一定会从队列中被排除。
• 如果该文件小于它后面hbase.hstore.compaction.max(默认为10)个文件之和乘 hbase.hstore.compaction.ratio(默认为1.2),则该文件也将加入到合并队列中。
Compaction带来的问题
对读的影响:
读性能会在 compaction 期间略微降低,而在 compaction 后又回到一个稳定的水平,从下图可以看到图中会有许多毛刺这是因为当进行 compaction 时读性能就会短暂的降低,而在完成后又 回到正常水平。
![图片来自 06HBase[完整版v1.0]-极客时间训练营,第 56 页](/2021/09/22/%E5%A4%A7%E6%95%B0%E6%8D%AE%E7%9A%84%E2%80%9C%E4%B8%89%E9%A9%BE%E9%A9%AC%E8%BD%A6%E2%80%9D%E4%B9%8B-HBase/图片来自 06HBase[完整版v1.0]-极客时间训练营,第 56 页.png)
对写的影响:
HFile个数超过 hbase.hstore.blockingStoreFiles(默认为7)时, 系统将会强制执行 compaction 操作进行文件合并, 此时写情况会被阻塞。在数据生成速度很快时,HFile的不断快速生成就需要进行频繁的 compaction 操作,从而限制写请求速度。
第二个问题是 compaction 操作会导致写放大。 一次写入的数据,被多次反复读取 与写入,会导致集群 IO 资源的浪费。
高可用和容灾
HDFS 数据备份
HBase本身是存储在 HDFS 上的,HDFS 默认是3备份,一份会写入本地节点,另外两个备份会被写入机甲感知得到的其它节点。WAL 和 HFiles 都会持久化到硬盘并备份。
Region Server故障恢复
当某个 Region Server 发生 crash 时,它所管理的 region 就无法被访问了,直到 crash 被检测到,然后故障恢复完成,这些 region 才能恢复访问。Zookeeper 依靠心跳检测发现节点故障,然后 HMaster 会收到 region server 故障的通知。
当 HMaster 发现某个 region server 故障,HMaster 会将这个 region server 所管理的 regions 分配给其它健康的 region servers。为了恢复故障的 region server 的 MemStore 中还未被持久化到 HFile 的数据,HMaster 会将 WAL 分割成几个文件,将它们保存在新的 region server 上。每个 region server 然后回放各自拿到的 WAL 碎片中的数据,来为它所分配到的新 region 建立 MemStore。

WAL 包含了一系列的修改操作,每个修改都表示一个 put 或者 delete 操作。这些修改按照时间顺序依次写入,持久化时它们被依次写入 WAL 文件的尾部。
当数据仍然在 MemStore 还未被持久化到 HFile 怎么办呢?WAL 文件会被回放。操作的方法是读取 WAL 文件,排序并添加所有的修改记录到 MemStore,最后 MemStore 会被刷写到 HFile。
HMaster HA
HMaster 有 HA 模式,即主备模式。同 一时间只有一个 HMaster 能成功在 Zookeeper 中注册 /hbase/master 节点,成为 Active 提供服务。
因为每台 HMaster 都和 Zookeeper 之间存在着心跳保持,当 Active HMaster 发生故障 时,Zookeeper 中的 /hbase/master 节点自动删除,其他 HMaster 此时如果成功注册 该节点,则变为新的 Active。成为 Active的 HMaster 需要从 Zookeeper 中加载完相应 的数据到内存,就可以提供服务。
在 HBase2.0 中,还有 HBase Read HA,Region 将不再只保存在某一单独的 Region Server 上,而是选择其他的 两个 Region Server 分别存储该 Region 的两个备份,这样某台 Region Server 挂掉时, 客户端仍然可以从其它 Region Server 上备份的 Region 中读到数据。
暂时就到这里啦~