前置问题与边界

HBase 能在 HDFS 上提供随机读写,不是因为 HDFS 获得了原地随机修改能力。HBase 把随机写接在 RegionServer 服务层:mutation 先经过 WAL 和 MemStore,随后 flush 成 HDFS 上不可变的 HFile。随机读也不是对 HDFS 文件做逐字节定位;客户端先定位 row key 所属 Region,再直连负责该 Region 的 RegionServer,由 RegionServer 合并 MemStore、BlockCache 和 HFile 中的结果。

本篇回答一个核心问题:HBase 为什么能在面向大文件的 HDFS 之上提供在线随机读写,代价落在哪里。row key 的 salt、反转和时间桶放到第 03 篇;WAL、MVCC、MemStore 与 flush 的时序放到第 04 篇;BlockCache、Bloom Filter 和 HFile 放到第 05、06 篇;Compaction、恢复、复制和运维会在后续篇目展开。

基础机制以 HBase 2.6.6、Hadoop 3.4.3、JDK 17 为基线。本篇不讨论大版本兼容性变化,只建立 2.6 路径下的基本对象关系。

HBase 是稀疏宽列模型。一个 Cell 的坐标可以表达为 (row, family, qualifier, timestamp):row key 决定排序和位置,column family 是预先定义的物理组织边界,qualifier 可以在同一列族内动态出现,timestamp 让同一坐标保存多个版本。这个模型不同于固定列的关系表,也不同于 Parquet、ORC 这类主要面向分析扫描的列存格式。

对象路径图

flowchart TD
  C[Client] -->|locate row key| M[hbase:meta]
  M -->|region location| C
  C -->|RPC| RS[RegionServer]
  RS --> R[Region: row-key range]
  R --> W[WAL on HDFS]
  R --> MS[MemStore]
  MS -->|flush| HF[HFile on HDFS]
  HF --> BC[BlockCache]
  HF --> CP[Compaction]

这张图有两个边界。第一,hbase:meta 提供 Region 位置信息,客户端拿到后会缓存;普通数据读写不经过 HMaster。第二,HDFS 保存 WAL 和 HFile,HBase 负责表、行、版本、Region、缓存和恢复语义,二者不能互相替代。

RowKey 是位置、排序和扫描键

row key 按字节字典序排序。这个排序决定一行数据落入哪个 Region,也决定相邻数据能否被一次连续 scan 读出。关系数据库的主键通常先被理解为唯一标识;HBase 的 row key 同时还是分布键和范围扫描键。

单调递增的时间戳前缀会把新写入推向末端 Region,扩容 RegionServer 也不能立即摊平一个热点 range。天然分散的用户前缀、反转域名、salt 前缀和时间桶都在改变“相邻”的含义。设计 row key 的任务不是让字符串好看,而是让常见访问路径在有序 key 空间里形成合理分布。

Region 是表的水平切分单位

一张表由多个 Region 组成,每个 Region 覆盖一段连续 row-key range。同一时刻,一个 Region 由一个 RegionServer 提供服务。表增长后,HBase 通过 split 把 key 空间拆成更多 Region,再由 assignment 和 balancer 把 Region 分散到不同 RegionServer。

Region 的存在解释了 HBase 的伸缩方式。增加节点只增加承载 Region 的位置;是否能摊开流量,仍取决于 row key 是否把请求分散到多个 range。一个永远只打到同一前缀的写入模型,即使集群有很多 RegionServer,也会先压住少数 Region。

WAL、MemStore 和 HFile 接住随机写

HDFS 擅长追加和顺序读写,不擅长频繁原地修改。HBase 的写路径把每次 mutation 记录到 WAL,并放入内存中的 MemStore。MemStore 达到阈值或被触发后,数据 flush 成 HDFS 上不可变的 HFile。

一次 Put 返回成功,只说明该 mutation 已按当前 durability 设置完成 HBase 写入协议。它不等于数据已经进入最终 HFile。flush 前的 RegionServer 崩溃依赖 WAL 重放恢复;flush 后的数据进入 HFile,但后台仍可能被 compaction 合并到新的 HFile。

读取成本由多个结构共同收敛

读取一行时,RegionServer 要合并 MemStore、BlockCache 和 HFile 中的候选 Cell。MemStore 保存尚未 flush 的新数据,BlockCache 缓存热点 block,HFile index 和 Bloom Filter 用来缩短文件查找路径。StoreFile 越多,读路径可能要检查的文件越多;BlockCache 命中率、Bloom Filter 配置和 compaction 进度会直接影响读延迟。

这也是 HBase 和分析型列存的分界。分析型列存主要优化大范围列扫描、压缩和向量化;HBase 优先服务按 row key 的随机读写和有序范围 scan。

关键对象和状态

对象 位置 作用 易错边界
RowKey 客户端与 RegionServer 定位、排序、范围扫描 不是只用于唯一标识
Region HBase 服务层 row-key range 分片 扩容机器不等于自动消除单 range 热点
hbase:meta HBase 系统表 保存 Region 位置 不是 ZooKeeper 中的全部元数据
RegionServer HBase 服务进程 承载 Region 读写 普通读写不经 HMaster
WAL HDFS 文件 崩溃恢复日志 durability 设置影响返回边界
MemStore RegionServer 内存 尚未 flush 的有序数据 成功返回不等于已进入 HFile
HFile HDFS 文件 flush 后不可变数据 StoreFile 数量影响读放大
BlockCache RegionServer 内存 缓存读 block 缓存不能消除所有磁盘访问
Compaction 后台任务 合并 StoreFile tombstone 和旧版本清理有条件

最小实验

UNVERIFIED_RUNTIME:当前任务环境未安装 HBase 2.6.6、Hadoop 3.4.3 和 JDK 17 组合,以下步骤只作为可复现实验,不提供伪造输出、耗时或日志。

1
hbase shell
1
create_namespace 'lab'
1
create 'lab:t00_rowkey_route', 'cf'
1
put 'lab:t00_rowkey_route', 'user#0003', 'cf:name', 'c'
1
put 'lab:t00_rowkey_route', 'user#0001', 'cf:name', 'a'
1
put 'lab:t00_rowkey_route', 'user#0002', 'cf:name', 'b'
1
scan 'lab:t00_rowkey_route'
1
scan 'lab:t00_rowkey_route', {STARTROW => 'user#0002', STOPROW => 'user#0100'}
1
flush 'lab:t00_rowkey_route'
1
hdfs dfs -ls -R /hbase/data/lab/t00_rowkey_route

这组步骤只观察三件事:scan 按 row key 字典序返回,STARTROWSTOPROW 表达连续 range,flush 后 HFile 位于 table、Region、column family 目录之下。STOPROW 是排他边界。若集群使用自定义 hbase.rootdir,HDFS 路径以实际配置为准。

清理命令:

1
disable 'lab:t00_rowkey_route'
1
drop 'lab:t00_rowkey_route'
1
drop_namespace 'lab'

失败恢复预告

写入返回后、MemStore 尚未 flush 前,RegionServer 崩溃会把问题交给 WAL、ServerCrashProcedure、WAL split/replay 和 Region reassignment。HDFS 只保证底层 WAL 文件和 HFile 副本,HBase 才负责把这些文件重新解释成表、Region 和 Cell 的可见状态。

复制链路也沿 WAL 展开。WAL edit 可以异步送到远端集群,但复制不是跨集群同步事务。第 12 篇会单独讨论顺序、延迟、冲突和故障切换。

工程迁移

HBase 机制 可迁移问题 相似系统
WAL + MemStore 随机写如何变成追加日志和内存结构 LSM memtable、redo log
HFile 持久化数据如何避免原地修改 SSTable、Lucene segment
Region 有序 key 空间如何切分和迁移 Tablet、Shard、Partition
BlockCache 热读如何避开磁盘 Page cache、query cache
Compaction 后台合并如何换前台读成本 SSTable compaction、segment merge

[PATTERN] 先把随机写变成追加日志和内存有序结构,再用不可变文件和后台合并收敛读放大。这个模式能迁移到 LSM 数据库、搜索引擎 segment 和日志结构存储,但不能迁移出相同的一致性语义。

常见误解

误解一:HBase 是 HDFS 的随机写插件。HBase 没有改变 HDFS 的原地修改能力,它在 HDFS 之上构建服务层。

误解二:HBase 表像 MySQL 表一样有固定列。HBase 的列族预定义,qualifier 可动态出现,不存在的 Cell 不占位。

误解三:HBase 是分析型列存数据库。HBase 按 column family 物理组织,但目标是在线随机读写和有序 scan,不是替代 Parquet/ORC。

误解四:ZooKeeper 保存所有 HBase 元数据。持久 Region 路由在 hbase:meta,协调状态和服务发现信息需要分开描述。

误解五:HBase 提供跨行事务。HBase 的普通强边界是单行原子性,不能扩展成跨行、跨表事务。

练习

  1. 为“按用户查询最近 30 天行为”设计一个 row key,并标出它会把热点推向哪里。

  2. 解释为什么时间戳作为 row key 前缀容易制造单 Region 写热点。

  3. 把一次 Put 拆成 WAL、MemStore、flush、HFile 四个阶段,标出每个阶段的崩溃恢复依据。

  4. 对比 HDFS 文件读取和 HBase 单行读取,指出 NameNode、DataNode、hbase:meta、RegionServer 各自出现的位置。

  5. 观察一张已有 HBase 表的 Region 列表,判断当前热点是否能由 row-key range 解释。

系列导航

篇号 主题 状态
00 导读:row key 决定数据位置 本篇
01 数据模型:Cell、Column Family 与版本 下一篇
02 集群架构:HMaster、RegionServer、ZooKeeper 与 hbase:meta
03 Schema 与 row key 设计
04 写入路径:WAL、MVCC、MemStore 与 flush
05 读取路径:BlockCache、Bloom Filter 与 HFile block
06 HFile 内部结构
07 Compaction、TTL、版本与 Delete
08 Region 生命周期:split、merge 与 assignment
09 Client 路由与重试
10 行级原子性与一致性边界
11 RegionServer 崩溃与 WAL 恢复
12 跨集群 Replication
13 Snapshot、Backup 与恢复
14 Get、Scan、Filter 与分页
15 BufferedMutator 与 Bulk Load
16 Coprocessor、Endpoint 与 Phoenix 边界
17 Kerberos、RPC 保护与 ACL
18 Metrics、hbtop、Compaction 与性能调优
19 HBase 3.0 与设计边界

参考资料