深入 HBase 06 - HFile 内部结构
前置问题与边界
本篇回答一个核心问题:data block、index、file info、Bloom metadata 如何支持 HBase 在不可变文件里做有序查找。
HFile 是 HBase flush 和 compaction 之后写到 HDFS 的 StoreFile 格式。它不是关系数据库页文件,也不是面向分析扫描的列存文件。它保存按 Cell key 排序的记录,并携带索引、元信息、Bloom 相关数据和 trailer,供 RegionServer 在读路径中缩小访问范围。
基线仍是 HBase 2.6.6。HBase 2.6.6 的代码支持 HFile format version 3,不能把当前实现误写成“只支持 HFile v2”。HFile v2 的文档解释了多级索引、Bloom 和 trailer 等关键结构;写作时要把格式演进和当前版本能力分开。
文件结构图
flowchart TD
H[HFile StoreFile] --> DB[Data Blocks: sorted Cells]
H --> LB[Leaf Index Blocks]
H --> IB[Intermediate and Root Index]
H --> BF[Bloom Blocks and Bloom Metadata]
H --> FI[FileInfo]
H --> MB[Meta Blocks]
H --> TR[Fixed File Trailer]
TR --> R[Reader opens trailer first]
R --> IB
R --> BF
R --> DB
RegionServer 打开 HFile 时,会先读取 trailer 和必要的元数据,获得索引根位置、压缩、编码、比较器和文件布局信息。实际读取某个 row key 时,reader 通过索引定位 data block,再把 block 解压、解码并在其中查找 Cell。
Cell key 与排序
HFile 的 data block 里保存 KeyValue 或 Cell 序列。Cell key 由 row、column family、qualifier、timestamp、type 等部分组成。排序规则让同一 row key、同一列族和限定符附近的版本聚在一起,并把较新的 timestamp 放在更容易被读取的位置。
这种排序支撑两个动作。第一,Get 可以在文件内部定位 row key 附近的 block。第二,多个 StoreFile、MemStore 和 snapshot 可以按同一套顺序做归并。只要所有来源保持相同排序,RegionServer 就能在读取时组合出某个 read point 下的可见结果。
Data block
Data block 是 HFile 里最常被读取的单位。一个 block 包含若干有序 Cell,可能经过压缩、校验和数据块编码。HBase 读取单个 Cell 时,底层通常仍以 block 为单位读取;block size 过大可能放大小读,过小则增加索引和元数据开销。
列族配置中的 block size 会影响 HFile data block 的大小。它不是 HDFS block size。HDFS block 是底层文件切分和副本单位;HFile block 是 HBase 文件格式内部的读缓存和索引单位。把二者混在一起,会误判调优方向。
Index block
HFile index 记录 data block 的 key 边界和文件偏移,使 reader 可以从 row key 走到可能包含该 key 的 block。HFile v2 之后的结构支持多级索引,较大的文件不需要把所有 data block 索引都平铺到一个巨大结构里。
索引并不等于二级索引。它只在一个 HFile 内部把 row-key 有序文件的查找范围缩小,不会让任意 qualifier 查询变成全局索引查询。HBase 没有因为 HFile index 就获得关系数据库式二级索引。
Bloom metadata
Bloom metadata 与 Bloom blocks 让 reader 在访问某个 HFile 前先做负向判断。对于随机 Get,如果 Bloom 判断 row 不在这个文件里,reader 可以跳过这个 HFile 的 data block 访问。对于 row+column 模式,Bloom 粒度更细,但写入和存储也有额外成本。
Bloom 数据和 HFile 索引配合工作。Bloom 负责判断“这个文件是否值得继续查”,索引负责“如果继续查,应该读哪个 block”。Bloom 命中并不返回 Cell;真正的 Cell 仍来自 data block。
FileInfo、Meta block 与 Trailer
FileInfo 保存文件级属性,例如平均 key 长度、最后 key、时间范围、entry 数、创建时间等。Meta block 保存 reader 需要的附加结构。Fixed File Trailer 放在文件末尾,提供打开文件时定位其他结构的入口。
读取 HFile 的典型入口是先读 trailer,再按 trailer 中的位置找到索引、FileInfo 和其他元数据。不可变文件把这些位置稳定下来,RegionServer 可以安全缓存 block 和元数据。compaction 会生成新的 HFile,而不是在旧 HFile 内原地重写 Cell。
HFile 与 LSM 读放大
MemStore flush 产生新的 HFile。写入越分散、flush 越频繁,StoreFile 数量越容易增加。读一个 row key 时,RegionServer 可能要检查多个 HFile;Bloom、index 和 BlockCache 只能减少每个文件内部或无效文件上的访问,不能把多个文件自动变成一个文件。
Compaction 的任务之一就是减少 StoreFile 数量,并在条件满足时处理旧版本、过期数据和 tombstone。第 07 篇会展开 compaction 条件;本篇只说明 HFile 的不可变性:旧文件不被就地修改,新文件通过 flush 或 compaction 产生。
最小实验
UNVERIFIED_RUNTIME:当前任务环境未安装目标版本 HBase、Hadoop 和 JDK 17。以下步骤只给复现实验,不给伪造输出。
创建最小表,并设置较小 block size,方便在测试环境观察 HFile block 相关指标。生产环境不应照抄该配置:
1 | |
1 | |
1 | |
写入多行数据:
1 | |
1 | |
1 | |
触发 flush:
1 | |
退出 shell 后查找表目录下的 HFile。/hbase 只是假定 rootdir,实际环境以 hbase.rootdir 为准:
1 | |
在安装了 HBase 工具的节点上,可以用 HFile 工具查看文件结构。下面的文件路径需要替换为实际 HFile 路径:
1 | |
这条命令用于观察 HFile 元信息,不应把示例里的路径、entry 数或 block 数写成固定结论。不同压缩、编码、block size 和写入数据都会改变输出。
清理实验表:
1 | |
1 | |
1 | |
Java Public API 观察边界
HFile 本身属于 HBase 内部存储格式,应用代码不应该依赖内部 reader 类来解析生产数据。Java Client Public API 应通过 Table、Get、Scan 和 Result 访问表数据。需要检查 HFile 结构时,应使用运维工具、测试环境或源码测试,而不是把内部类当成应用接口。
下面代码只演示使用 Public API 读取同一行,不直接解析 HFile:
1 | |
UNVERIFIED_RUNTIME:本轮未编译运行该示例。它只使用 HBase Client Public API,避免把内部 HFile reader 写成稳定应用接口。
失败恢复路径
HFile 写入完成后由 HDFS 保存副本,由 HBase 通过 Region、StoreFile 列表和 meta 信息管理服务语义。RegionServer 崩溃时,未 flush 的数据依赖 WAL 回放;已经形成的 HFile 不需要重新从客户端写入。恢复后,新接管 Region 的 RegionServer 会打开现有 StoreFile,并重新建立必要的 reader、block cache 热度和 scanner 状态。
如果 compaction 正在进行,HBase 使用临时文件、提交步骤和 StoreFile 列表更新来保证旧文件和新文件的切换可恢复。旧 HFile 的清理还会受 snapshot、引用文件和归档目录影响。第 13 篇会说明 Snapshot 对文件生命周期的影响。
工程迁移
| HFile 结构 | 可迁移模式 | 迁移场景 |
|---|---|---|
| Data block | 固定读放大单位 | SSTable block、列存 page、对象存储 range |
| Sparse index | 有序文件定位 | LSM 文件索引、segment term index |
| Bloom metadata | 负向排除 | 缓存穿透、文件级 min/max 与 bloom |
| Trailer | 尾部目录入口 | Parquet footer、ORC footer、segment manifest |
模式公式是:immutable_file = sorted_blocks + index + filters + footer。听到“文件不可变但要支持随机查找”时,应先寻找 footer、索引和过滤器,而不是期待底层文件系统支持原地更新。
常见误解
| 误解 | 更准确的说法 |
|---|---|
| HFile block 就是 HDFS block | HFile block 是文件格式内部单位;HDFS block 是底层文件系统单位 |
| HFile index 是全局二级索引 | HFile index 只定位单个文件内部的有序 data block |
| HBase 2.6 只能写 HFile v2 | 2.6.6 代码支持 HFile format version 3,文档中的 v2 结构不能被写成当前上限 |
| 应用应该直接读 HFile | 应用使用 Public API;HFile 工具和内部 reader 属于运维与源码层 |
| compaction 会修改旧 HFile | compaction 生成新 HFile,再更新 StoreFile 集合 |
练习
- 调整列族 block size,在测试环境观察 HFile 工具展示的 block 数变化。
- 分别开启和关闭 Bloom Filter,比较不存在 row 的
Get会触发哪些读路径指标。 - 连续 flush 生成多个 HFile,再通过 major compaction 观察 StoreFile 数量变化。
- 阅读 HBase 2.6.6 源码中的 HFile trailer 和 block reader,标出哪些类属于内部 API。
系列导航
参考资料
- Apache HBase HFile Format:https://hbase.apache.org/docs/hfile-format/
- Apache HBase Reference Guide:HFile、BlockCache、Bloom Filter:https://hbase.apache.org/docs/
- Apache HBase 2.6 Developer API:HFile、HFileBlock、BlockCache:https://hbase.apache.org/2.6/devapidocs/
- Apache HBase 源码 rel/2.6.6:HFile reader、HFileBlock、FixedFileTrailer:https://github.com/apache/hbase/tree/rel/2.6.6
- LSM-Tree 论文:https://www.cs.umb.edu/~poneil/lsmtree.pdf
