前置问题与边界

本篇回答一个核心问题: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
hbase shell
1
create_namespace 'lab'
1
create 'lab:t06_hfile', {NAME => 'cf', BLOCKSIZE => '16384', BLOOMFILTER => 'ROW'}

写入多行数据:

1
put 'lab:t06_hfile', 'user#0001', 'cf:name', 'alice'
1
put 'lab:t06_hfile', 'user#0002', 'cf:name', 'bob'
1
put 'lab:t06_hfile', 'user#0003', 'cf:name', 'carol'

触发 flush:

1
flush 'lab:t06_hfile'

退出 shell 后查找表目录下的 HFile。/hbase 只是假定 rootdir,实际环境以 hbase.rootdir 为准:

1
hdfs dfs -ls -R /hbase/data/lab/t06_hfile

在安装了 HBase 工具的节点上,可以用 HFile 工具查看文件结构。下面的文件路径需要替换为实际 HFile 路径:

1
hbase org.apache.hadoop.hbase.io.hfile.HFile -m -f hdfs://namenode:8020/hbase/data/lab/t06_hfile/<region>/cf/<hfile>

这条命令用于观察 HFile 元信息,不应把示例里的路径、entry 数或 block 数写成固定结论。不同压缩、编码、block size 和写入数据都会改变输出。

清理实验表:

1
disable 'lab:t06_hfile'
1
drop 'lab:t06_hfile'
1
drop_namespace 'lab'

Java Public API 观察边界

HFile 本身属于 HBase 内部存储格式,应用代码不应该依赖内部 reader 类来解析生产数据。Java Client Public API 应通过 TableGetScanResult 访问表数据。需要检查 HFile 结构时,应使用运维工具、测试环境或源码测试,而不是把内部类当成应用接口。

下面代码只演示使用 Public API 读取同一行,不直接解析 HFile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;

public final class HBaseHFileBoundaryExample {
public static void main(String[] args) throws IOException {
Configuration conf = HBaseConfiguration.create();
try (Connection connection = ConnectionFactory.createConnection(conf);
Table table = connection.getTable(TableName.valueOf("lab:t06_hfile"))) {
Result result = table.get(new Get(Bytes.toBytes("user#0001")));
System.out.println(result.size());
}
}
}

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 集合

练习

  1. 调整列族 block size,在测试环境观察 HFile 工具展示的 block 数变化。
  2. 分别开启和关闭 Bloom Filter,比较不存在 row 的 Get 会触发哪些读路径指标。
  3. 连续 flush 生成多个 HFile,再通过 major compaction 观察 StoreFile 数量变化。
  4. 阅读 HBase 2.6.6 源码中的 HFile trailer 和 block reader,标出哪些类属于内部 API。

系列导航

编号 文章
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 与设计边界

参考资料