2.3 HFile:落盘格式与多版本


文档摘要

2.3 HFile:落盘的格式与多版本的物理真相 本节摘要:HFile 是 HBase 的磁盘存储格式,本质是按行键排序的 KeyValue 序列加上多级索引与可选布隆过滤器。本节拆解 HFile 的分层结构,展示用工具查看真实文件的方法,并讲清多版本、墓碑、TTL 在文件内的物理形态——这直接决定第 3 章读路径为什么要"合并"。 从 KeyValue 说起 HFile 里没有"行"这个容器,只有一条条 KeyValue 记录。1.2 节的四维坐标,在物理上被编码进每条 KeyValue 的键部分: 同一个行键的多列数据,就是多条 RowKey 前缀相同的 KeyValue 连续存放;多版本则是 RowKey、列均相同、仅时间戳不同的多条记录。

2.3 HFile:落盘的格式与多版本的物理真相

本节摘要:HFile 是 HBase 的磁盘存储格式,本质是按行键排序的 KeyValue 序列加上多级索引与可选布隆过滤器。本节拆解 HFile 的分层结构,展示用工具查看真实文件的方法,并讲清多版本、墓碑、TTL 在文件内的物理形态——这直接决定第 3 章读路径为什么要"合并"。

从 KeyValue 说起

HFile 里没有"行"这个容器,只有一条条 KeyValue 记录。1.2 节的四维坐标,在物理上被编码进每条 KeyValue 的键部分:

KeyValue 布局(概念示意) +------------------------------------------------------------+ | key length | value length | key | +------------------------------------------------------------+ key = rowkey + column family + qualifier + timestamp + type type ∈ { Put, DeleteColumn, DeleteFamily ... } value = 原始字节

同一个行键的多列数据,就是多条 RowKey 前缀相同的 KeyValue 连续存放;多版本则是 RowKey、列均相同、仅时间戳不同的多条记录。排序规则是全字段字典序:先比行键,再比列族、限定符,最后时间戳大的排前面——最新版本永远在同键记录的头部,读取时拿第一条即可。2.2 节"MemStore 是跳表、Flush 天然有序"到这里闭环:HFile 就是有序 KeyValue 流的容器。

HFile 的分层结构

一个 HFile 自上而下分块组织,块大小默认 64 KB(建表时 BLOCKSIZE 可调,与 HDFS 块无关,别混淆):

图 2.3-1 HFile 文件内部布局

图 2.3-1 HFile 文件内部布局

要点解读:

  • Trailer 在文件末尾,记录各段偏移与版本信息,打开 HFile 第一步先读它(一次 IO);
  • 多层索引让单文件可以长到几十 GB 而点查仍是"根 → 叶 → 块"的少量 IO:根索引常驻内存,叶子索引按需加载;
  • 布隆过滤器分形存储(每几个数据块一段),用于回答"这个行键一定不在本文件吗",读路径的大杀器,第 6 章单独展开;
  • 块压缩默认开启(SNAPPY 等算法,列族级参数 COMPRESSION),行键前缀高度重复恰好是压缩友好型数据,1.2 节"行键重复出现在每条 KeyValue 上"的空间代价被大幅抵消。

块大小与压缩:建表时的两笔账

分层结构里有两个列族级参数值得单独算账。

BLOCKSIZE 的权衡。默认 64 KB 是随机点查与顺序扫描的折中:块越小,点查搬运的无关数据越少、索引越密(占内存越多);块越大,顺序扫描吞吐越高、索引越稀。两个极端场景各有最优解——随机点查为主的表调小到 16 或 32 KB(索引项虽多但每次 IO 更省),大范围扫描为主的表调大到 128 KB。改它要慎重:块大小决定了已落盘文件的物理形态,改参数只影响之后 Flush 的新文件,想让全表生效得靠一次 Major Compaction 重写。

COMPRESSION 的选择。常用三档:SNAPPY 压缩率中等但编解码极快,CPU 便宜 IO 贵的在线集群几乎无脑选它;GZ 压缩率高但慢,适合写一次读多次的归档数据(配合冷表很划算);LZ4 与 SNAPPY 同级、具体谁快看硬件。一个常被忽略的联动:压缩发生在块级别,块内相邻 KeyValue 的行键前缀高度重复,所以大块压缩率更高——块大小与压缩选择不是两个独立决策。另外布隆过滤器与索引不参与数据块压缩,它们本来就是为省 IO 设计的紧凑结构。

💡 线上经验:多数业务用 SNAPPY 加默认块大小就到头了。先按默认建表,等监控(第 7 章)显示 IO 成为瓶颈再回头调这两个参数——有指标之后再调,与拍脑袋调,是两种工程。

用工具看真实文件

灌完数据后(2.2 节的实验接着用),可以离线检视 HFile。用 HBase 自带的 HFile 工具(在 HBase 目录下执行):

$ bin/hbase hfile -v -p -m \ file:///opt/hbase-data/hbase/data/default/flush_demo/<region-hash>/cf/<hfile-hash> ... Scanning -> u1001-1724055123/cf:status/1724055600/Put/vlen=4/seqid=12 value = PAID ... Bloom filter: Bloom filter type: ROW Number of keys: 200000 Max keys: 218971 ... Block index: Number of blocks: 1543 Total size: 522 KB

输出逐条列出了 KeyValue(注意 Put 类型与 seqid,正好对应 2.1 节的 sequence id),以及布隆过滤器条目数与块索引规模。给同一条目做"更新",再 Flush,新 HFile 里会出现同键不同时间戳的新记录,旧记录仍在旧文件——更新与删除在 HFile 层面都只是追加新记录

墓碑、TTL 与"删了还占地方"

删除的物理实现是写入一条 type 为 Delete 系列的记录(墓碑)。它比同键的旧 Put 排得靠前(排序规则里删除标记优先),读路径扫描时碰到墓碑就知道后面的旧值作废。但旧值仍然躺在旧文件里,要等 Compaction 重写文件时才被物理清除(第 3 章)。

TTL 同理:列族可设生命周期(单位秒),到期的 Cell 在 Compaction 时被丢弃。一个典型陷阱:

hbase:030:0> alter 'flush_demo', {NAME => 'cf', TTL => '86400'}

设完 TTL 观察磁盘占用并不下降——因为清理发生在下一次 Major Compaction。想立刻见效需手动触发 major_compaction。同样的道理,"疯狂 delete 后磁盘不缩水"也是 Compaction 时机问题,不是 bug。

⚠️ 常见坑:把大量短生命周期的数据写进同一张表又靠 delete 清理,墓碑本身也占空间,还拖慢读路径扫描。正确做法是按时间分表或靠 TTL 加 Major Compaction 兜底。

版本数与数据保留的三个旋钮

旋钮 参数 效果
保留版本数 VERSIONS 读取与 Compaction 时最多留几个时间戳
最少版本数 MIN_VERSIONS 配合 TTL,过期也保底留几版
生命周期 TTL Compaction 时清除超龄 Cell

默认 VERSIONS=1 意味着读路径只需关心"最新一条",行为最简单。把 VERSIONS 调大(如 5)后,get 要返回多条、Scan 语义也变复杂,第 3 章读路径的多版本合并会用到这些设定。

💡 关键直觉:HFile 是"只追加、不修改"的,一切更新删除都是新记录加标记,真正的整理发生在 Compaction——把这句话记住,第 3 章的读放大与合并开销都会顺理成章。

本节要点回顾

  • 物理无行只有 KeyValue:行键、列族、限定符、时间戳、类型全部编进每条记录的键;
  • 排序全字段字典序:最新版本排最前,删除标记优先于同键数据;
  • 分层结构:Trailer、根/叶索引、布隆块、64 KB 数据块,大文件也能低 IO 点查;
  • 更新删除皆追加:墓碑与 TTL 都要等 Compaction 才物理生效,磁盘不缩水是机制不是故障;
  • 版本三旋钮:VERSIONS、MIN_VERSIONS、TTL 共同决定一条历史数据活多久。

写路径三站到此走完。但 Store 里已经攒下了好几个 HFile,同一条数据散落多处——下一章先讲 Region 的分裂与 Compaction 怎么收拾局面,再把读路径的三层合并补全。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U