HBase 的 HFile 是数据在 HDFS 上持久化的最终形态。每一次刷写(Flush)或合并(Compaction)都会生成新的 HFile,整个集群的查询性能很大程度上取决于 HFile 的组织方式。要真正掌握 HBase,就必须了解 HFile 格式的设计意图,而不仅仅是会用命令行工具查看它。

HFile 的整体物理布局
HFile 从 HBase 1.0 开始统一采用 V2 格式(HFile V2),该版本引入了更灵活的 Block 类型和多层索引。逻辑上,一个 HFile 被划分为四个主要区域:Scanned Block 区、Non-Scanned Block 区、Load-On-Open 区以及 Trailer。Scanned Block 区包含所有的 Data Block,这些数据块是请求扫描时会被顺序读取的内容。Non-Scanned Block 区主要是 Leaf Index Block 和 Bloom Block,只有进行随机查找时才会根据索引定位到它们。Load-On-Open 区保存 Root Index Block、File Info 以及 Trailer 中的字段副本,这样在打开 HFile 时只需一次读取就能获得文件元数据和根索引。
物理上,这些区域是按顺序写入的:最前面是若干个 Data Block,接着是 Data Block 的索引(Leaf Index 和 Root Index),随后是 Meta Block(通常包含 Bloom Filter 数据块),再后面是 File Info,最后以 60 字节固定长度的 Trailer 结尾。Trailer 就像一本书的目录,记录了各区域的偏移量、版本号、压缩算法以及文件加密等信息。HFile 解析的第一步必然是读取 Trailer,通过其中的 majorVersion 字段确定版本,然后读取 loadOnOpenOffset 获取 File Info 的位置,进而拿到所有 Data Block 的分布详情。
为了方便理解,我们可以将 Trailer 的字段抽象如下:
// HFile Trailer 关键字段(大端序存储)
public class HFileTrailer {
long majorVersion; // 文件主版本,V2 为 2
long minorVersion; // 次版本,如 3
long numDataIndexEntries; // Data Index 条目数
long numMetaIndexEntries; // Meta Index 条目数
long loadOnOpenDataOffset; // File Info 起始偏移
long dataIndexOffset; // Root Data Index 起始偏移
long metaIndexOffset; // Root Meta Index 起始偏移
long totalUncompressedBytes;// 未压缩总字节数
int compressionCodec; // 压缩算法代号,如 LZO=1, LZ4=5
byte[] fileInfoKey; // 固定 8 字节,如“HFileInf”
byte[] trailerMagic; // 固定魔数,如“TRABLOCK 00 00”
}
魔数 TRABLOCK\000\000 是确认 HFile 完整性的重要标识。HFile 解析器会从文件尾部向前扫描,寻找该魔数,从而反推出 Trailer 的开始位置。这种尾部元信息的组织方式(类似 TCP 分段)使得新版本可以自由扩展字段而不破坏旧版本的读取能力。
Data Block 与 KeyValue 存储格式
Data Block 是数据存储的基本单元,默认大小 64KB。每一个 Data Block 内部由若干个连续的 KeyValue 记录组成。HBase 的 KeyValue 结构非常紧凑,它由 Key Length、Value Length、Row Key、Column Family、Qualifier、Timestamp、Key Type 以及 Value 等字段构成。这种设计将整个“坐标”信息编码在一条记录里,使得序列化和反序列化都很快。
实际的二进制布局如下:一个 4 字节的 int 表示 Key 的长度(包含 Row、Family、Qualifier、Timestamp、KeyType 但不含 Value 长度),接着是 4 字节的 Value 长度,然后是 Key 部分。Key 部分又细分为:变长的 Row Key(通过两个 Byte 的长度前缀划定),紧接着一个字节的 Column Family 长度,再是 Column Family 字节数组,随后是 Qualifier 长度与 Qualifier 字节数组,然后是 8 字节的大端序 Timestamp,最后 1 个字节的 KeyType(Put、Delete、DeleteColumn 等)。Value 则紧接着放在 Key 之后。这种无分隔符、纯粹靠长度前缀解析的格式非常适合顺序扫描和二分查找,但代价是无法直接在字节流上快速“跳过”不需要的列,必须逐个 KeyValue 遍历。
为了缩减存储空间,HBase 还引入了 Data Block Encoding(编码)机制,例如 Prefix Encoding 和 Diff Encoding。启用编码后,Data Block 内部的 KeyValue 不再是完整独立的记录,而是基于前一条记录的差异进行压缩存储。常见的 FAST_DIFF 编码会提取行键、列族和 Cell 级别的公共前缀,有效降低键部分的冗余。但这种编码会带来额外的 CPU 开销,因此通常仅在写密集场景下通过 Compaction 时进行离线编码,以减少对读路径的影响。
下面这段伪代码展示了如何从一个字节缓冲区中读取单个 KeyValue 的关键部分:
ByteBuffer buf = ...; // 指向 Data Block 内当前位置 int keyLength = buf.getInt(); int valueLength = buf.getInt(); // 标记 Key 的起始位置,方便后面整体略过 int keyStart = buf.position(); short rowLength = buf.getShort(); byte[] row = new byte[rowLength]; buf.get(row); byte familyLength = buf.get(); byte[] family = new byte[familyLength & 0xff]; buf.get(family); int qualifierLength = buf.getInt(); // V2 中 Qualifier 使用 4 字节长度 byte[] qualifier = new byte[qualifierLength]; buf.get(qualifier); long timestamp = buf.getLong(); byte type = buf.get(); // 跳过 Key 剩余部分(如果 keyLength 还包括其他预留字段) buf.position(keyStart + keyLength); byte[] value = new byte[valueLength]; buf.get(value);
需要注意的是,Column Family 的长度只使用了 1 个字节,因此最大长度限制为 255。而 Qualifier 长度使用 4 个字节,足以容纳非常长的列名。Timestamp 按降序存储,这意味着同一个 Cell 的最新版本会排在最前面,这正好与 HBase 读取“最新版本优先”的语义吻合。
索引结构与布隆过滤器
单纯依靠顺序扫描 Data Block 无法支撑随机查询,因此 HFile 构建了多级索引。每个 Data Block 都有一个 Block Index Entry,记录该 Block 的第一个 Key(可选也可以是其它的分隔 Key)、Block 在文件中的偏移量、未压缩大小以及 Block 的布隆过滤器索引(若有)。这些 Entry 聚合起来形成 Leaf Index Block,多个 Leaf Index Block 又由 Root Index Block 索引。Root Index Block 在文件打开时被加载到内存,当需要定位 Row Key 时,HBase 会先从 Root Index 开始二分查找,定位到某个 Leaf Index,再从中找到具体的 Data Block。这种两级索引结构将内存占用控制在了很低水平,同时保证了查找效率。
Meta Block 区域主要用于存放布隆过滤器数据。HBase 支持 ROW 级别的布隆过滤器和 ROWCOL 级别的布隆过滤器。布隆过滤器以 Block 为单位存在,每个 Data Block 都可以关联一个布隆过滤器,用来快速判断某个 Row Key 或 Row+Column 组合是否可能存在于该 Block。它的存储方式是将位数组按照固定大小分块,并记入 Meta Index。当 Get 请求到来时,系统会首先使用布隆过滤器过滤掉大部分不相关的 HFile,这也正是为什么开启布隆过滤器能极大减少磁盘扫描的核心原因。
从文件格式角度看,布隆过滤器的数据块由几个头部字段和一个连续的位数组组成。头部包含布隆过滤器类型(BYTE_ARRAY 或 ROWCOL)、哈希函数数量、Key 个数以及位数组大小。位数组以冗余的方式写入,以确保即使发生磁盘 bit 翻转也能在一定程度上检错。在 HFile V2 中,Bloom Block 和 Data Block 的索引是分离的:Data Block 有 Data Index,Bloom Block 有 Meta Index。Trailer 中分别记录了二者的偏移量,加载时可以根据 usesBloomFilter 标记按需读取。
综合来看,HFile 格式通过 Block 分区、两级索引和可插拔的布隆过滤器,在随机读和顺序扫之间取得了平衡。理解其文件格式不仅有助于分析 RegionServer 的启动速度和内存占用,还能在进行 Pre-Split 或 Major Compaction 时更有针对性地调整参数。如果你正在开发 HBase 的运维工具或者想对 HFile 进行离线分析,掌握上述二进制布局将是最坚实的第一步。