本文是在翻译自 这两篇 很棒的关于 HBase 架构简介文章的基础上写的。

HBase 是一个运行在 Hadoop 集群中的数据库。

它不是传统的关系型数据库,所以缺少 ACID 等事务相关特性,但也因此带来了比 RDBMS 更强的扩展性

同时,HBase 中保存的数据也不必像 RDBMS 一样必须满足 schema 的限制,因此很适合存储非结构化或者半结构化的数据。

HBase 设计

HBase 的易扩展性来自于其 — 被一起访问的数据也存在一起 — 的设计模式:根据 将数据聚合,并且将不同的数据集存在集群中不同的节点上。在获取数据的时候,从同一个节点上获取数据集,效率很高。

HBase 被称为 面向列簇 的数据库,但同时也是 面向行 的: 每一行都由键索引,根据这个键可以查询每行存储的数据。每个列簇都保存了一些类似的数据,可以把每一行数据理解成所列簇中的所有数据的集合,如下图所示。

HBase 也被认为是一个分布式数据库。根据来聚合数据并将不同数据集分布在集群中不同的节点上,其中,
每个 Key 是数据更新的最小单元。

HBase 数据模型

在 HBase 中,数据都以 RowKey 来确认位置并按序存储。排序在 HBase 设计中非常重要。

每张表根据 RowKey 范围 被分割为由多行数据集组成的序列集,被称为 Regions。这些 Regions 被存储在集群中的不同节点中,这些节点被称为 RegionServer。这种模式可以通过增加 RegionServer 的数量来有效扩展读写性能。

HBase 的表中每个列簇都被存在不同的文件中,可以被单独访问,其映射关系如下图所示:

数据都被存在 HBase 表的 Cell 中。一个完整的 Cell 被称为一个键值对:

可以简单地理解为: 一个数据 ValueTable:Row:ColumnFamily:ColumnName:Timestamp 定位.

HBase 表中数据的逻辑存储和物理存储的映射关系如下图所示:

HBase 的表一般情况下都是稀疏表,每个 Cell 中的数据都是有版本的,可以使用 Timestamp 或者自定义版本号系统。一个 Put 操作可以表示 插入 和 更新 两种操作,每种操作都会生成新的版本。
Delete 操作会生成一个 tombstone marker,它可以防止 Get 请求时返回对应的数据。在 Get 请求时,如果没有声明版本号,会返回最新的版本。每个 Cell 会默认保存 3 个版本的数据,除非你显示修改配置。

HBase 架构组件

HBase 集群由三种类型的组件组成:

  • Region Servers: 管理数据的读写;
    在访问数据时,客户端直接和 Region Server 交互;
  • HBase Master: 管理 Region 分配,执行 DDL 操作;
  • ZooKepper: 维护集群状态;

Region Server 管理的数据就存放在 HDFS 集群中的 DataNode 上,遵循着 数据本地性 的原则,HBase 数据的读写都在本地进行,除非有 Region 被移走了。

HDFS 的 NameNode 负责管理数据的元信息。

Regions

如上文所说,HBase 表中的数据根据 RowKey 范围水平分割进多个 Region

一个 Region 包含了表中从 Region 起始 Key 到结束 Key 的所有行数据

Regions 被分配到集群中的 RegionServer 里,数据的读写都由 RegionServer 负责。

HBase HMaster

HBase HMaster 负责集群中 Region 的分配,表的 DDL 操作:

  • 协调 Region Servers

    • 在集群启动时分配 Region,在集群失败恢复时或者需要负载均衡时重新分配 Region;
    • 监测集群中所有的 RegionServer,同时监听来自 zookeeper 的通知;
  • Admin 函数

    • 负责对表的 DDL 操作。

ZooKeeper: 协调器

HBase 使用 ZooKeeper 作为分布式协调服务,用于维护集群中服务器状态。

ZooKeeper 用于维护机器的死活,并且在机器挂的时候发出通知。ZooKeeper 使用共识算法来保证集群中的共享状态。

协作

ZooKeeper 用于协调分布式系统中成员的共享状态信息。RegionServer 和 活的 HMaster 通过一段 会话 和 ZK 连接。ZK 通过 心跳 给活跃的会话创建暂时节点

每个 Region Server 都会创建一个 暂时节点,HMaster 会监测这些节点来发现可用的 RegionServer,同时也监测失败的节点。

多个 HMaster 争相创建自己的暂时节点,ZK 决定第一个创建成功的机器为真正的 HMasterHMaster 也像 ZK 发送心跳包,而那些未激活的 HMaster 则监听真正 HMaster 的失败消息。

如果一个 RegionServer 或者 HMaster 发送心跳包失败了,那么会话就过期了,相应的暂时节点也被删除,监听消息者会收到这个删除的消息:

  • HMaster 会监听 Region Server 的失败消息,一旦收到消息,就开始启动 恢复程序;
  • 备用 HMaster 监听真正 HMaster 的失败消息,一旦收到,就启动新的选举算法。

HBase 第一次读写

当有客户端第一次向 HBase 读或写时:

  1. 客户端从 ZK 得到保存 META 表的 RS 的地址;
  2. 客户端访问这个 RS 上的 META 表,得到想要查询的 RowKey 所在的 RS 地址;随后,客户端将 META 表地址缓存起来;
  3. 从对应的 RS 拿到数据。
  4. 这以后的读操作都从缓存中获取 META Server 地址。

HBase Meta 表

  • META 表保存着系统中所有 regions 信息;
  • META 表类似 B 树;
  • META 表的结构如下:
    • Key: region start key, region id
    • Values: RegionServer

Region Server 组件

每个运行在 DataNode 上的 RegionServer 都有这些组件:

  • WAL: Write-Ahead-Log 是一份用于存储还未被持久化的数据的文件,它主要用于失败恢复;
  • BlockCache: 是一种读缓存。用于在内存中缓存经常被读的数据。Least Recently Used (LRU) 数据在存满时会被失效;
  • MemStore: 是一种写缓存;
    • 用于缓存还未被持久化到硬盘的数据;
    • 在持久化之前会先将数据排序;
    • 每个 region 的每个 CF 都有一个 MemStore;
  • Hfile: 真正存在硬盘上的,对行数据排好序的 键值对 文件。

HBase 写操作步骤

当 HBase 收到一个 Put 请求时:

  1. 首先写数据到 WAL 中:数据修改会被追加到 WAL 文件的末尾;
  2. 数据被放在 MemStore 中,然后给客户端返回一个 PUT ack
  3. 数据在 MemStore 中以有序的 键值对 的形式存储;每个 CF 都有一个 MemStore
  4. MemStore 中的数据量达到配置的容量时,整个数据集都被写入到 HDFS 中的 HFile;一个 CF 有多个 HFile,每个 HFile 存储的是有序键值对;同时,每个 HFile 中还存储了最后被写入的序列数作为一个元信息,这个信息反映了 CF 中持久化到哪里了;

HBase HFile

数据从 MemStore 写入 HFile 的过程是一个 序列写 操作,序列写操作非常快,因为避免了磁头寻址的过程;

HBase HFile 结构

HFile 包含了一个多层索引,索引可以让 HBase 在不遍历整个 HFile 的情况下得到数据,多层索引就像一颗 B+树:

  • 键值对以升序状态存储;
  • 索引指向 64KB数据块,数据块中存着数据;
  • 每个数据块都有一个 叶子索引;
  • 每个数据块的最后一个 key 被放在 中间索引;
  • 根索引指向中间索引

每个 HFile 都有一个 trailertrailer 被写在 HFile 文件的末尾,并会指向 HFile 的 Meta Blocks。Trailer 还包含了 bloomfilters 和时间范围等信息。布隆过滤器可以帮助跳过那些不包含某个 RowKey 的 HFile,时间范围信息也是同样的作用。

HFile 索引

索引在 HFile 被加载时就被存入内存中,准确的说是 BlockCache 中,这可以保证每一次读文件只需一个磁盘寻址;

HBase 读合并

客户端在 Get 或者 Scan 时,步骤如下:

  1. 在 Block Cache 中找。BlockCache 是读缓存。
  2. 在 MemStore 中找。MemStore 中包含最新的写数据。
  3. 通过 Block Cache 中保存的 索引和布隆过滤器 将 HFiles 加载进内存中,再找。

由于每个 MemStore 会有多个 HFiles,这就意味着对于一个读请求,会检查多个 HFiles,而这会影响查询性能。这种影响被称为读放大

HBase Minor Compaction

为了解决上面的问题,HBase 会自动将一些 HFiles 通过合并排序合并为一个大的 HFiles,这个过程被称为 Minor Compaction

HBase Major Compaction

HBase Major Compaction 会将所有的 HFiles 写进一个超大的HFile,在这个过程中,会将 deletes过期 的数据丢掉。这可以提升读性能,但会带来大量的 磁盘 IO 和网络 IO,这种影响称为 写放大

Region == 连续的行数据

回顾一下:

  • 每张表被水平分割成一个或者多个 Region。每个 Region 都包含了连续的有序的一组行数据;
  • 每个 Region 都直接向 client 提供读写服务;

Region 分割

最开始的时候每张表有一个 Region,当一个 Region 的数据量超过设定值后,会分割成两个子 region。这两个代表了原始 Region 一半的 子 Region 在同一个 RegionServer 里并行运着,然后这次分割被上报给 HMaster

另外,为了负载均衡的考虑,HMaster 可能会将新生成的 region 移到其他 RegionServer 上。这时,在 Region 上的读写都不再是本地读写了,而需要通过网络 IO,直到 Major Compaction 发生。


HDFS 数据备份

由于 HFile 都存放在 HDFS 中,所以所有被持久化到硬盘的 HFiles 都依赖 HDFS 的文件副本。

而那些没有被持久化的数据都依赖于 WAL。

HBase 崩溃恢复

当有一个 RegionServer 失败时,ZK 会监测到心跳包丢失并决定说这个节点挂掉了,然后 HMaster 就知道了。
这时 HMaster 会决定好新的多个 RS 并将这个失败的 RS 上的 WAL 文件分割成多分然后将它们放到新的 RS 上,然后每个新的 RS 会回放这些 WAL 文件以重构 memstore

HBase 数据恢复

WAL 文件包含了一系列的更新操作,而每个更新操作都代表了一个 put 或者 delete 操作。

通过读 WAL 文件,可以实现数据恢复:读 WAL 然后 把数据向 Memstore 添加并排序。最后的最后,MemStore 中的数据会被写到磁盘的 HFile 中。

一点小总结

HBase 架构有这些优点:

  • 强一致模型
    当数据被写后,所有客户端都能读到更新后的数据。
  • 自动扩展
    • 当数据量太大时,Region 会自动分割;
    • 使用 HDFS 作为数据分散和备份;
  • 内置恢复机制
    使用 WAL 来保证数据不丢失;
  • 和 Hadoop 集成
    MR on HBase 是很自然了。

HBase 架构的问题:

  • 连续可靠性
    • WAL 回放太慢;
    • 崩溃恢复机制太过复杂;
    • Major Compaction 会带来巨大的 IO 消耗。