深入 HBase 01 - 数据模型:Cell、Column Family 与版本
前置问题与边界
HBase 的一个值不是由“第几行第几列”定位,而是由 (row, family, qualifier, timestamp) 这组坐标定位。row 是 uninterpreted bytes,按字典序排序;family 在建表时定义,是存储、压缩、缓存、版本数和 TTL 等配置的边界;qualifier 是 family 之内的列限定符,可以动态出现;timestamp 是版本维度。
本篇回答一个核心问题:(row, family, qualifier, timestamp) 如何唯一定位一个值。写入路径、MVCC、WAL 和 flush 放到第 04 篇;Delete tombstone 在 compaction 中真正清理的条件放到第 07 篇;行级原子性放到第 10 篇。
HBase 的“宽列”容易被误写成两种东西。它不是关系数据库里的稀疏宽表,因为 qualifier 不需要全表预定义,也不存在空列占位。它也不是分析型列存,因为物理按 column family 组织 Store,目标是按 row key 的在线读写和范围 scan。
Cell 坐标图
flowchart LR
R[row key bytes] --> C[Column]
C --> F[column family]
C --> Q[column qualifier]
Q --> T[timestamp/version]
T --> V[value bytes]
官方 Data Model 将 column 描述为 family:qualifier。Cell 是 row、column family、column qualifier 的组合,并包含 value 与 timestamp。另一个更适合实现层的说法是 {row, column, version} 精确指定一个 Cell;其中 column 又由 family 和 qualifier 组成。
Row:排序键不是业务字段
row key 是字节数组,HBase 不解释它的业务含义,只按字典序排序。这个排序影响 Region 范围、scan 范围和热点分布。字符串、数字、时间戳、复合 key 都只是编码方案,最终都要落成 bytes。
如果业务把 row key 设计成 20260829#userId,所有同一时间前缀的写入会挤在相邻 key 空间。若设计成 userId#reverseTimestamp,同一用户的近期数据容易被连续 scan,但跨用户按时间聚合就不再天然连续。row key 设计的本质是选择哪类查询获得连续性,哪类查询承担二次索引或离线计算成本。
Column Family:物理组织和配置边界
column family 必须在 schema 定义时声明。所有属于同一 family 的 qualifier 物理上存放在一起,并共享压缩、BlockCache、Bloom Filter、TTL、版本数等存储属性。官方文档也建议同一 family 内的数据具有相近访问模式和大小特征。
这意味着 family 不是“业务模块分组”的装饰字段。把冷热数据、大小差异极大的字段、访问频率完全不同的字段塞进同一 family,会让缓存、压缩和 compaction 难以按真实访问模式工作。反过来,family 也不宜随意增多,因为每个 Region 下每个 family 都对应 Store,StoreFile 数量、flush 和 compaction 成本都会被放大。
Qualifier:动态列但不是无约束 schema
qualifier 可以在同一个 family 里动态出现。cf:email、cf:phone、cf:lastLogin 不需要预先在建表语句中列出。这个特性常被说成 schema-less,但准确说法是:HBase 的 qualifier 维度灵活,column family、row key、版本策略和访问模式仍然构成 schema。
动态 qualifier 适合属性稀疏、列名本身带业务含义的场景,例如用户画像标签、倒排索引 posting、事件属性集合。它不适合把每个时间点、每个用户或每个大对象都塞进 qualifier;那会把排序能力从 row key 空间挪走,scan 和热点治理反而变难。
Timestamp:版本坐标
timestamp 是 long 类型版本号。未显式指定 timestamp 时,HBase 通常使用 RegionServer 写入时的时间;也可以在 Put 中显式写入 timestamp。读请求默认返回最新可见版本;如果需要多个版本,必须在 column family 版本数配置和 Get/Scan 的 readVersions 设置上同时满足条件。
HBase 按 timestamp 降序组织版本,因此读取最新版本时可以先遇到较新的 Cell。多个写入使用相同 row、family、qualifier、timestamp 时,官方文档说明最后写入的值是可读取的值。这个“last write wins”不是跨行事务,也不是跨集群冲突解决策略,只是同一 Cell 版本坐标内的覆盖语义。
Delete:先写标记,再等清理机会
Delete 不会原地删除 HFile 中的旧值。HBase 使用 tombstone 标记删除语义,读路径在合并 Cell 时根据 tombstone 过滤旧值。真正移除 tombstone 和被覆盖旧值,需要等到满足条件的 compaction。TTL、版本数、快照引用和 compaction 类型都会影响何时能物理清理。
本篇只讲模型边界。第 07 篇会展开 minor compaction、major compaction、TTL、版本和 Delete marker 之间的细节。
关键对象和状态
| 对象 | 定义 | 状态变化 | 约束 |
|---|---|---|---|
| Table | row 的有序集合 | split 成多个 Region | schema 定义 family,不定义每个 qualifier |
| Row | row key 与若干 Cell | 按 row key 排序 | row 内 mutation 是原子边界 |
| Column Family | qualifier 集合的物理前缀 | Store、StoreFile、cache 配置随 family 生效 | 不能当作无限业务分组 |
| Qualifier | family 内列名 | 可以动态出现 | 列名过长会进入每个 Cell 的存储成本 |
| Timestamp | 版本号 | 读默认取最新版本 | 显式 timestamp 会影响可见版本顺序 |
| Cell | 坐标和值 | Put 写入,Delete 标记过滤 | value 是 uninterpreted bytes |
| Tombstone | 删除标记 | compaction 条件满足后清理 | 不是立即物理删除 |
最小实验
UNVERIFIED_RUNTIME:当前任务环境未运行 HBase 2.6.6、Hadoop 3.4.3、JDK 17 组合。以下实验用于复现版本与 Delete 语义,不提供伪造输出。
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
这组步骤观察四件事:同一 row/family/qualifier 可以有多个 timestamp;默认 get 只取最新可见版本;VERSIONS => 3 让多个版本可被读取;删除某个版本后,读路径会按删除标记过滤,而不是立即改写旧 HFile。
清理命令:
1 | |
1 | |
1 | |
Java Public API 示例
UNVERIFIED_RUNTIME:以下代码只使用 HBase 2.6 公共 client API,未在本任务环境连接真实集群执行。
1 | |
示例只说明坐标表达方式:Put 面向单行,addColumn 指定 family、qualifier、timestamp 和 value;Get 指定同一行与列,并用 readVersions 请求多个版本。是否能读到多个版本,还取决于表的 column family 版本配置。
失败恢复
数据模型本身不负责恢复,但它决定 WAL replay 和读路径如何合并结果。WAL edit 记录的是 mutation;重放后仍然按照 row、family、qualifier、timestamp 形成 Cell。Delete 也是一种 marker,恢复后同样参与可见性判断。
因此,故障恢复不能写成“把文件恢复回来就结束”。HDFS 保住 WAL 和 HFile 副本后,HBase 还要把 mutation、版本、Delete marker 和 MVCC 可见性还原成表语义。
工程迁移
| HBase 模型 | 迁移时要保留的问题 | 不能直接迁移的结论 |
|---|---|---|
| RowKey 排序 | 查询是否能变成连续 key range | 所有数据库都适合复合字符串 key |
| Column Family | 物理组织是否按访问模式分组 | family 越多越灵活 |
| Qualifier 动态 | 稀疏属性如何编码 | schema-less 等于没有 schema |
| Timestamp 版本 | 多版本读取和保留策略 | 版本等于事务时间 |
| Tombstone | 删除和物理清理分离 | delete 后立刻释放空间 |
[PATTERN] 宽列模型的关键是把“逻辑列灵活”与“物理边界稳定”分开。动态 qualifier 提供表达能力,column family 和 row key 负责控制存储成本。
常见误解
误解一:HBase 没有 schema。HBase 不要求预定义 qualifier,但 family、row key、版本和 TTL 都是 schema。
误解二:空列会占空间。官方文档明确空 Cell 不存在,也不占用存储空间。
误解三:timestamp 总是物理写入时间。未指定时通常使用 RegionServer 写入时间;显式 timestamp 可以由客户端指定。
误解四:Delete 立即删除旧值。Delete 先产生 tombstone,物理清理依赖 compaction 条件。
误解五:同一行像关系数据库记录一样有固定列。HBase row 可以拥有不同 qualifier 集合。
练习
-
给用户画像表设计两个 column family,并说明每个 family 的访问频率和 TTL 是否相近。
-
解释为什么把每一天写成 qualifier 可能让按时间范围 scan 变困难。
-
对同一 Cell 写入三个 timestamp,分别说明默认 get、指定版本数 get、指定时间范围 get 的语义差异。
-
判断一个“动态字段很多”的业务是适合 qualifier,还是更适合把字段名编码进 row key。
-
说明 Delete marker 为什么必须等 compaction 才可能物理清理。
系列导航
| 篇号 | 主题 | 状态 |
|---|---|---|
| 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 与设计边界 |
参考资料
- Apache HBase Data Model:https://hbase.apache.org/docs/datamodel/
- Apache HBase 2.6 Client Package API:https://hbase.apache.org/2.6/apidocs/org/apache/hadoop/hbase/client/package-summary.html
- Apache HBase 2.6 Put API:https://hbase.apache.org/2.6/devapidocs/org/apache/hadoop/hbase/client/Put.html
- Apache HBase 2.6 Get API:https://hbase.apache.org/2.6/devapidocs/org/apache/hadoop/hbase/client/Get.html
- Apache HBase 2.6 Delete API:https://hbase.apache.org/2.6/devapidocs/org/apache/hadoop/hbase/client/Delete.html
- Apache HBase ACID Semantics:https://hbase.apache.org/acid-semantics/
- Apache HBase HFile Format:https://hbase.apache.org/docs/hfile-format/
