ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HBase 架构详解:存储架构、系统架构与读写流程(db-tutorial 实战解读)

HBase 架构详解:存储架构、系统架构与读写流程(db-tutorial 实战解读)
  • 文档
  • 教程
  • 数据库

【免费下载链接】db-tutorial

📚 后端程序员应该掌握的主流数据库知识

项目地址:https://gitcode.com/gh_mirrors/db/db-tutorial
点击查看免费下载

HBase 是一个在 HDFS 上开发的面向列的分布式数据库。本文以 db-tutorial 仓库中 HBase 架构 为核心骨架,结合仓库内 javadb-hbase 模块 的源码实现,从存储架构、系统架构、读写流程三个层面完整剖析 HBase 的内部机制。读完本文,你将掌握 Region / RegionServer 的存储单元划分原理、Master / ZooKeeper 的协调与故障恢复机制、WAL / MemStore / HFile / BlockCache 四类核心组件的协作关系,以及一条数据从客户端写入到落盘、从请求发出到返回的完整链路,并能对照仓库中的 Java 客户端封装代码验证这些原理。

一、HBase 架构总览

HBase 的架构设计源于 Google 的 Bigtable 论文,其核心思想可以概括为三点:

  • 表被水平切分为 Region 分布在集群中:一张表的数据按 Row Key 范围拆成多个更小的块,分散存储在不同的服务器上,这些小块叫做Region,存放 Region 的服务器叫做RegionServer。
  • Master / Slave 主从架构:Master 进程负责在不同 RegionServer 之间分发 Region,并协调管理所有 RegionServer 节点;RegionServer 则直接对外提供数据读写服务。
  • 依赖 ZooKeeper 与 HDFS:ZooKeeper 负责集群协调与故障发现,HDFS 负责最终的数据持久化存储。

从文件角度看,HBase 主要处理两种文件:

文件类型全称作用存储位置
WALWrite Ahead Log(预写日志)存储尚未持久化到 HFile 的数据记录,用于故障恢复默认位于/hbase/WALs/目录下
HFileHBase 数据文件将行数据以 Key/Value 形式最终存储在文件系统上HFile 存储的根目录默认为/hbase

仓库中的客户端封装对此有直接印证:HbaseFactory.java 在构造客户端配置时显式设置了hbase.zookeeper.quorum=127.0.0.1、hbase.zookeeper.property.clientPort=2181和hbase.rootdir=/hbase,说明一个 HBase 客户端要工作,至少要同时连上 ZooKeeper 协调服务和指向 HDFS 上的根目录。

一个基本的读写寻址流程是:客户端首先联系 ZooKeeper 集群查找行键(Row Key)。该过程通过 ZooKeeper 获取含有-ROOT-的 Region 服务器来完成;通过含有-ROOT-的 Region 服务器可以查询到.META.表中对应的 Region 服务器名(其中包含请求的行键信息)。这两种内容都会被缓存下来,并且只查询一次。最终,通过查询.META.服务器来获取客户端查询的行键数据所在 Region 的服务器名。(注:在新版本 HBase 中-ROOT-表已被移除,寻址入口直接由 ZooKeeper 指向hbase:meta表。)

二、HBase 存储架构

2.1 Region:表的分片单元

HBase Table 中的所有行按照Row Key的字典序排列。HBase Table 根据 Row Key 的范围分片,每个分片叫做Region。一个Region包含了在 start key 和 end key 之间的所有行。

关于 Row Key 的字典序,需要注意以下几点(详见仓库中 02.HBase数据模型.md):

  • 字典序对整型字符串排序的结果是1,10,100,11,12,...,2,20,21,...,因此如果使用整型字符串作为行键,为了保持整型的自然序,行键必须用0做左填充。
  • 行的一次读写操作是原子性的(不论一次读写多少列)。
  • 所有对表的访问都要通过 Row Key,包括指定 Row Key 访问、Row Key 范围访问、全表扫描三种方式。

仓库中 RowKeyUtil.java 的getRowKeyForOriginId正是用StrUtil.padPre(bizId, length, "0")对原始 ID 做左填充补零,以保持字典序与业务序一致;而 RowType.java 则枚举了四种主键策略:

  • ORIGIN_ID:原 ID(左填充补零,保证字典序);
  • TIMESTAMP:以 10 位秒级时间戳作为 ID,数据存储单调递增,适合以 scan 为主、数据量不大(100 万以内)、读频率不高的场景;
  • UUID:UUID 主键,数据存储离散,适合数据量较大且以 get 为主的场景;
  • BUCKET:ID = bucket(2~3位) + timestamp(10位) + bizId,适合数据量较大且需要大量 scan 的场景(需在@RowKeyRule中指定分桶数)。

HBase 支持自动分区:每个表初始只有一个Region,随着数据不断增加,Region会不断增大,当增大到一个阈值的时候,Region就会分裂为两个新的Region。当 Table 中的行不断增多,就会有越来越多的Region。

Region是 HBase 中分布式存储和负载均衡的最小单元。这意味着不同的Region可以分布在不同的RegionServer上,但一个Region不会被拆分到多个 Server 上——Region 是迁移、分裂、负载均衡的原子粒度。

除了自动分区,HBase 也支持创建表时预分区(预置 split keys),避免单 Region 热点。HbaseAdmin.java 的createTable(TableName, byte[][] splitKeys, String... families)就是通过admin.createTable(td, splitKeys)在初始化时按拆分关键字一次性创建多个 Region,这是规避"单 Region 写入热点"的常用手段。

2.2 Region Server:Region 的宿主

Region只是表被拆分的逻辑单元,真正运行它们的是Region Server。RegionServer 运行在 HDFS 的 DataNode 上,它包含以下核心组件:

WAL(Write Ahead Log,预写日志)
  • 用于存储尚未持久化到 HFile 的数据记录,以便在发生故障时进行恢复。如果写 WAL 失败了,那么修改数据的完整操作就是失败的。
  • 通常情况下,每个 RegionServer 只有一个 WAL 实例。在 HBase 2.0 之前,WAL 的实现叫做 HLog。
  • WAL 位于/hbase/WALs/目录下(与 HbaseFactory.java 中hbase.rootdir=/hbase的设置相呼应)。
  • 关于 MultiWAL:如果每个 RegionServer 只有一个 WAL,由于 HDFS 必须是连续的,必须顺序写 WAL,这会产生性能瓶颈。MultiWAL 允许 RegionServer 通过 HDFS 底层的多管道并行写多个 WAL,最终提升总的吞吐量,但不会提升单个 Region 的吞吐量。

仓库客户端在 HbaseTemplate.java 中提供了incrementColumnValue原子自增方法,其默认持久化级别即为Durability.SYNC_WAL——即数据必须同步刷入 WAL 才算写入成功,这正是 WAL 在写入路径上"先落日志、再改内存"的体现。

BlockCache:读缓存

读缓存。它将频繁读取的数据存储在内存中,如果存储不足,它将按照**最近最少使用原则(LRU)**清除多余的数据。相应地,BaseScan.java 在构造底层Scan时设置了scan.setCaching(1000)(RPC 一次拉取 1000 行)和scan.setCacheBlocks(false)(批量扫描场景下跳过读缓存,避免污染 BlockCache),这说明 BlockCache 对读性能的影响是客户端可以主动控制的。

MemStore:写缓存

写缓存。它存储尚未写入磁盘的新数据,并会在数据写入磁盘之前对其进行排序。每个 Region 上的每个列族(Column Family)都有一个 MemStore。写入先落在 MemStore 中排序,达到阈值后再批量刷写(Flush)成 HFile,从而把随机写转化为顺序写。

HFile:最终落盘的数据文件

将行数据按照 Key/Value 的形式存储在文件系统上。HFile 是 HBase 在 HDFS 中存储数据的格式,它包含多层的索引,这样 HBase 在检索数据的时候就不用完全加载整个文件。索引的大小(keys 的大小、数据量的大小)影响 block 的大小,在大数据集的情况下,block 的大小设置为每个 RegionServer 1GB 也是常见的。

HFile 的生成过程(从 MemStore 到 HFile):

  1. 起初,HFile 中并没有任何 Block,数据还存在于 MemStore 中。
  2. Flush 发生时,创建 HFile Writer,第一个空的 Data Block 出现,初始化后的 Data Block 中为 Header 部分预留了空间,Header 部分用来存放一个 Data Block 的元数据信息。
  3. 而后,位于 MemStore 中的 KeyValues 被一个个 append 到位于内存中的第一个 Data Block 中。
Region 与 Store / StoreFile / HFile 的对应关系

RegionServer 存取一个子表时,会创建一个 Region 对象,然后对表的每个列族创建一个Store实例,每个Store会有 0 个或多个StoreFile与之对应,每个StoreFile则对应一个HFile,HFile 就是实际存储在 HDFS 上的文件。整体嵌套关系为:

Table └── Region(按 Row Key 范围分片) └── Store(每个列族一个) └── StoreFile(0 个或多个) └── HFile(实际存储在 HDFS 上的文件)

仓库中 HbaseTemplate.java 的newPut在构造Put时按Map<列族, Map<列, 值>>两级结构组织数据,天然对应了"Region → Store(列族) → 列"的物理模型;测试用例 HbaseTemplateGetTest.java 中同一条行键分别写入f1、f2两个列族,读取时也分别按列族查询,正是"每个列族一个 Store"的客户端印证。

三、HBase 系统架构

和 HDFS、YARN 一样,HBase 也遵循 master / slave 架构:

  • HBase 有一个 master 节点,master 节点负责协调管理 RegionServer 节点:
    • master 负责将 Region 分配给 RegionServer 节点;
    • master 负责恢复 RegionServer 节点的故障。
  • HBase 有多个 RegionServer 节点,RegionServer 节点负责零个或多个 Region 的管理并响应客户端的读写请求,还负责 Region 的划分并通知 master 节点有了新的子 Region。
  • HBase 依赖 ZooKeeper 来实现故障恢复。

3.1 Master Server

Master Server 负责协调 RegionServer,具体职责如下:

  • 为 RegionServer 分配 Region;
  • 负责 RegionServer 的负载均衡;
  • 发现失效的 RegionServer 并重新分配其上的 Region;
  • HDFS 上的垃圾文件回收;
  • 处理 Schema 的更新请求(建表、删表、修改列族等 DDL 操作)。

对照仓库,HbaseAdmin.java 封装的创建命名空间、创建/删除/禁用/启用表、列出所有表等管理操作,最终都通过Admin接口提交给 Master 执行,这就是"Master 处理 Schema 更新请求"的客户端视角。

3.2 Region Server

  • RegionServer 负责维护 Master 分配给它的 Region,并处理发送到 Region 上的 IO 请求(读写都由 RegionServer 直接响应客户端,不经过 Master);
  • 当 Region 过大,RegionServer 负责自动分区(Region Split),并通知 Master 记录更新。

也就是说,读写数据不经过 Master,Master 只负责"调度"不负责"数据",这保证了 HBase 集群的数据通路不会在 Master 上形成瓶颈。

3.3 ZooKeeper:分布式协调服务

HBase 依赖 ZooKeeper 作为分布式协调服务来维护集群中的服务器状态。ZooKeeper 维护哪些服务器是活动的和可用的,并提供服务器故障通知。集群至少应该有 3 个节点(保证 ZooKeeper 自身的高可用)。

ZooKeeper 在 HBase 中的作用:

  • 保证任何时候,集群中只有一个 Master;
  • 存储所有 Region 的寻址入口(hbase:meta表的位置信息);
  • 实时监控 RegionServer 的状态,将 RegionServer 的上线和下线信息实时通知给 Master;
  • 存储 HBase 的 Schema,包括有哪些 Table、每个 Table 有哪些 Column Family 等信息。

仓库中 HbaseFactory.java 必须配置hbase.zookeeper.quorum与hbase.zookeeper.property.clientPort才能建立连接,且 HbaseTemplate.java 通过ConnectionFactory.createConnection(configuration, ...)创建连接——HBase 客户端正是通过 ZooKeeper 获取集群元数据(meta 表位置、服务器列表)来引导连接建立的。

Master 选举机制

以上职责中,最重要的一点是:ZooKeeper 如何保证 HBase 集群中只有一个 Master Server?

  1. 所有 Master Server 会竞争 ZooKeeper 的 znode 锁(一个临时节点),只有一个 Master Server 能够创建成功,此时该 Master 就是主 Master。
  2. 主 Master 会定期向 ZooKeeper 发送心跳。从 Master 则通过 Watcher 机制对主 Master 所在节点进行监听。
  3. 如果主 Master 未能及时发送心跳,则其持有的 ZooKeeper 会话会过期,相应的 znode 锁(临时节点)会被自动删除。这会触发定义在该节点上的 Watcher 事件,所有从 Master 会得到通知,并再次开始竞争 znode 锁,直到完成新主 Master 的选举。

这套"临时节点 + 会话心跳 + Watcher 通知"的机制,保证了任何时刻集群中有且仅有一个 Active Master,且在 Master 宕机后能自动完成故障切换。

hbase:meta 目录表

HBase 内部保留名为hbase:meta的特殊目录表(catalog table,即上文提到的.META.表)。它维护着当前集群上所有 Region 的列表、状态和位置。hbase:meta表中的项使用 Region 作为键。Region 名由所属的表名、Region 的起始行、Region 的创建时间以及基于整体计算得出的 MD5 组成。

hbase:meta表的位置信息本身存储在 ZooKeeper 上,客户端通过"ZooKeeper → hbase:meta → 目标 RegionServer"的两级寻址找到数据所在位置。

四、HBase 读写流程

4.1 写入数据的流程

一次典型的写入请求按以下步骤执行:

  1. Client 向 RegionServer 提交写请求;
  2. RegionServer 找到目标 Region;
  3. Region 检查数据是否与 Schema 一致(列族是否存在、数据格式是否合法);
  4. 如果客户端没有指定版本,则获取当前系统时间作为数据版本(时间戳);
  5. 将更新写入 WAL Log;
  6. 将更新写入 MemStore;
  7. 判断 MemStore 存储是否已满,如果存储已满则需要 Flush 为 Store HFile 文件。

这套流程在仓库客户端代码中可以得到完整印证:

  • 步骤 4(时间戳):HbaseTemplate.java 的newPut中,当调用方未传timestamp时自动取System.currentTimeMillis()作为数据版本;
  • 步骤 5、6(先 WAL 后 MemStore):HBase 客户端构造的Put经 RPC 到达 RegionServer 后,服务端会先追加 WAL、再写入 MemStore,若 WAL 写入失败则整个写入操作失败(对应上文Durability.SYNC_WAL的默认持久化语义);
  • 步骤 7(Flush):MemStore 达到阈值后触发 Flush,将排序后的数据生成 HFile 落盘,并清空 MemStore、生成新的 HFile。

仓库中的写入 API 覆盖了从单条写入到批量写入的完整场景:HbaseTemplate.java 提供了put的单列/多列/列族/实体/批量(batchPut)多种重载,测试用例 HbaseTemplateGetTest.java 展示了"写入 → 读取校验 → 删除"的完整闭环验证。

4.2 读取数据的流程

以下是客户端首次读写 HBase 上数据的流程:

  1. 客户端从 ZooKeeper 获取META表所在的 RegionServer;
  2. 客户端访问META表所在的 RegionServer,从META表中查询到访问行键所在的 RegionServer,之后客户端将缓存这些信息以及META表的位置;
  3. 客户端从行键所在的 RegionServer 上获取数据。

如果再次读取,客户端将从缓存中获取行键所在的 RegionServer。这样客户端就不需要再次查询META表,除非 Region 移动(分裂、迁移、故障恢复)导致缓存失效,此时则会重新查询并更新缓存。

注:META表是 HBase 中一张特殊的表,它保存了所有 Region 的位置信息,META表自己的位置信息则存储在 ZooKeeper 上。这也是为什么 HBase 客户端必须首先连接 ZooKeeper——见 HbaseFactory.java。

读取流程在仓库客户端中的对应实现:

  • 按行键精确读取(Get):HbaseTemplate.java 提供get、batchGet、getEntity、getEntityList等方法,底层通过Table.get(Get)完成;
  • 按行键范围扫描(Scan):HbaseTemplate.java 提供scan、分页page、滚动scroll等方法,BaseScan.java 负责将startRow/stopRow、时间范围、reversed等参数转换为底层Scan对象——Scan 本质上就是按 Row Key 区间在对应 Region 上顺序扫描;
  • 两级缓存:HBase 客户端自身会缓存hbase:meta的位置与 Region 与 RegionServer 的映射(避免每次请求都访问 ZooKeeper),RegionServer 端则有 BlockCache 缓存热数据块,两者共同支撑"第二次读取更快"的体验。

测试用例 HbaseTemplateScanTest.java 先批量写入 100 条数据,再通过page/scroll接口分页读取并逐页校验,验证了"Region 按 Row Key 范围有序存储 → 范围扫描结果有序"这一架构特性。

五、架构要点总结

关注点核心结论
数据分片表按 Row Key 字典序切分为 Region,Region 是分布式存储与负载均衡的最小单元,支持自动分裂与预分区
存储模型每个列族一个 Store(MemStore + 若干 StoreFile/HFile),写入先 WAL 后 MemStore,Flush 生成 HFile
读写缓存BlockCache(LRU 读缓存)+ MemStore(写缓存),配合 Scan 的 caching/cacheBlocks 参数调优
主从架构Master 只做调度(分配 Region、负载均衡、故障恢复、Schema 更新),RegionServer 直接响应读写
协调服务ZooKeeper 用临时节点 + Watcher 保证单 Master、维护 meta 寻址入口、监控 RegionServer 状态
寻址链路Client → ZooKeeper → hbase:meta → 目标 RegionServer,两级寻址并带客户端缓存
写入链路Client → RegionServer → 校验 Schema → 分配时间戳 → WAL → MemStore →(满时)Flush 成 HFile
读取链路首次经 meta 定位 RegionServer,之后走缓存;Get 精确读、Scan 范围读

六、延伸阅读

如果你希望进一步深入 HBase 的学习与实践,可以继续阅读本仓库 HBase 教程目录下的其他文档:

  • HBase 快速入门
  • HBase 数据模型(Row Key 字典序、列族、Cell、时间戳等逻辑模型细节)
  • HBase Schema 设计
  • HBase Java API 基础特性(结合 javadb-hbase 模块 的 Mapper/Template 封装实践)
  • HBase 运维(单点/分布式模式下hbase-site.xml与 ZooKeeper 的实际配置示例)
  • HBase 命令
  • 文档
  • 教程
  • 数据库

【免费下载链接】db-tutorial

📚 后端程序员应该掌握的主流数据库知识

项目地址:https://gitcode.com/gh_mirrors/db/db-tutorial
点击查看免费下载

相关推荐

上一篇:KeysPerSecond:终极键盘手速监控神器,解锁你的操作极限表现
下一篇:解决Hoppscotch自托管GraphQL路径错误:从配置到调试的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进