- 文档
- 教程
- 数据库
【免费下载链接】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 主要处理两种文件:
| 文件类型 | 全称 | 作用 | 存储位置 |
|---|---|---|---|
| WAL | Write Ahead Log(预写日志) | 存储尚未持久化到 HFile 的数据记录,用于故障恢复 | 默认位于/hbase/WALs/目录下 |
| HFile | HBase 数据文件 | 将行数据以 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):
- 起初,HFile 中并没有任何 Block,数据还存在于 MemStore 中。
- Flush 发生时,创建 HFile Writer,第一个空的 Data Block 出现,初始化后的 Data Block 中为 Header 部分预留了空间,Header 部分用来存放一个 Data Block 的元数据信息。
- 而后,位于 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?
- 所有 Master Server 会竞争 ZooKeeper 的 znode 锁(一个临时节点),只有一个 Master Server 能够创建成功,此时该 Master 就是主 Master。
- 主 Master 会定期向 ZooKeeper 发送心跳。从 Master 则通过 Watcher 机制对主 Master 所在节点进行监听。
- 如果主 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 写入数据的流程
一次典型的写入请求按以下步骤执行:
- Client 向 RegionServer 提交写请求;
- RegionServer 找到目标 Region;
- Region 检查数据是否与 Schema 一致(列族是否存在、数据格式是否合法);
- 如果客户端没有指定版本,则获取当前系统时间作为数据版本(时间戳);
- 将更新写入 WAL Log;
- 将更新写入 MemStore;
- 判断 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 上数据的流程:
- 客户端从 ZooKeeper 获取
META表所在的 RegionServer; - 客户端访问
META表所在的 RegionServer,从META表中查询到访问行键所在的 RegionServer,之后客户端将缓存这些信息以及META表的位置; - 客户端从行键所在的 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
📚 后端程序员应该掌握的主流数据库知识
相关推荐
HBase 列式数据库深度剖析:db-tutorial 架构设计详解
HBase 列式数据库深度剖析:db tutorial 架构设计详解 HBase 作为 Hadoop 生态系统中的核心组件,是一个构建在 HDFS 之上的分布式
文档教程数据库CubeFS 纠删码(EC)子系统 BlobStore 设计详解:架构、数据读写流程与存储引擎原理
CubeFS 纠删码(EC)子系统 BlobStore 设计详解:架构、数据读写流程与存储引擎原理 导读 CubeFS 在 3.0.0 版本中正式引入了纠删码(
存储分布式文件系统对象存储云原生Apache Hudi RFC-100 深度解读:BLOB 非结构化数据统一存储架构与读写设计
Apache Hudi RFC 100 深度解读:BLOB 非结构化数据统一存储架构与读写设计 导读 本文以 Apache Hudi 社区 RFC 100(Un
数据湖湖仓一体大数据数据存储
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考