深入 HBase 05 - 读取路径:BlockCache、Bloom Filter 与 HFile block
前置问题与边界
本篇回答一个核心问题:Get 如何减少磁盘读取,并把 MemStore、BlockCache 与多个 HFile 的结果合成同一行的可见 Cell。
HBase 读路径不是 HDFS 随机读能力的直接暴露。客户端先定位 row key 所属 Region,再把请求发到对应 RegionServer;RegionServer 在 Store 内部合并内存数据和不可变 StoreFile。HDFS 提供文件读取和副本,HBase 提供 row-key 定位、HFile 索引、Bloom Filter、BlockCache 和版本可见性。
本篇基线是 HBase 2.6.6、Hadoop 3.4.3、JDK 17。HBase 3.0.0 的集中变化只放在第 19 篇。
读取路径图
flowchart TD
C[Client Get row key] --> M[Region location from cache or hbase:meta]
M --> RS[RegionServer]
RS --> R[Region]
R --> S[Store per Column Family]
S --> MS[MemStore snapshot and active MemStore]
S --> BC[BlockCache]
S --> BF[Bloom Filter]
S --> HF[HFile blocks on HDFS]
MS --> MV[Merge by Cell order and read point]
BC --> MV
BF --> HF
HF --> MV
MV --> OUT[Result]
Get 的核心不是只找一个文件。一个 column family 可能已经 flush 过多次,形成多个 HFile;MemStore 里也可能存在还没 flush 的最新写入。RegionServer 必须按 Cell 排序和版本规则合并这些来源,最后返回符合 Get 条件的结果。
关键对象和状态
| 对象 | 位置 | 读路径职责 | 状态风险 |
|---|---|---|---|
Connection |
客户端进程 | 复用 RPC、线程和缓存 | 频繁创建会增加连接与定位成本 |
hbase:meta |
HBase 系统表 | 保存 Region 位置 | Region 移动后旧缓存需要刷新 |
| Region | RegionServer | 接收对应 row-key range 的读写 | assignment 变化会触发重试 |
| Store | Region 内 column family 维度 | 合并 MemStore 和 StoreFile | family 是物理读放大的边界 |
| MemStore | RegionServer 内存 | 提供尚未 flush 的可见 Cell | read point 决定可见范围 |
| BlockCache | RegionServer 内存 | 缓存 HFile block | 命中率低会把压力转回 HDFS |
| Bloom Filter | HFile 相关元数据 | 对不存在的 row 或 row+column 做负向排除 | 只能排除,不能证明存在 |
| HFile block | HDFS 文件内部块 | 保存有序 Cell 与索引可定位数据 | 小范围读仍会以 block 为单位读取 |
Store 是理解 HBase 读放大的关键。一个表有多个 column family 时,同一行的不同 family 物理上落在不同 Store;一次只读某个 family 可以避开其他 family 的 StoreFile。反过来,把冷热字段塞进同一个 family,会让冷字段的历史文件也参与热读路径。
Get 的合并过程
Get 到达 RegionServer 后,Region 先确认 row key 落在本 Region 的范围内,再把请求分发到相关 Store。每个 Store 会检查 MemStore、snapshot、StoreFile scanner,并依据 read point 和版本规则挑出可见 Cell。
MemStore 优先提供还没有 flush 的写入。flush 只把内存快照写成新的 HFile,不改变已成功写入的逻辑可见性;可见性由写入协议、MVCC read point 和操作语义共同决定。
HFile 路径分成几层过滤。Bloom Filter 用于判断某个 row 或 row+column 是否不在文件里。命中 Bloom 不等于 Cell 必然存在,因为 Bloom 允许假阳性;未命中才可以跳过对应文件。随后 HFile index 把查找范围缩小到少量 block。BlockCache 命中时,RegionServer 可以直接使用缓存里的 block;未命中时再从 HDFS 读取 block 并按策略放入缓存。
最后,RegionServer 按 HBase 的 Cell 排序、timestamp 和 Delete 标记处理结果。第 07 篇会展开 tombstone 与版本清理;本篇只保留读路径事实:历史值、删除标记和多个 StoreFile 在读时仍可能同时参与合并。
BlockCache 的边界
HBase 2.x 的 BlockCache 接口之下存在多种实现和组合形态,包括常见的 LruBlockCache、BucketCache 以及组合缓存。不同部署会按内存、堆外空间和 GC 风险选择实现。文章不把某一种实现写成唯一默认,也不把 BlockCache 写成操作系统 page cache 的同义词。
BlockCache 缓存的是 HFile block,不是任意 Java 对象。数据 block、index block、Bloom 相关 block 的缓存策略不同,具体是否缓存取决于表配置、列族配置、访问 API 和块类型。Scan#setCacheBlocks(false) 常用于一次性大扫描,避免大范围顺序读把在线热点 block 挤出缓存。
缓存能缩短热读路径,但不能消除读放大。StoreFile 数量很多时,即使大部分 block 命中,RegionServer 仍要维护多个 scanner、处理多路归并,并承担版本与 Delete 可见性判断。读优化不能只盯 BlockCache 命中率,还要看 StoreFile 数量、Bloom 命中形态、row key 分布和 compaction backlog。
Bloom Filter 的边界
Bloom Filter 是用空间换掉一部分无效文件检查的概率结构。HBase 的 Bloom Filter 可以配置在 column family 层面,用于 row 或 row+column 粒度的查找优化。它适合 Get 或很窄的随机读,不适合被写成“让所有 scan 都变快”的万能索引。
Bloom Filter 的结论只有负向排除最可靠。如果判断“不存在”,对应 StoreFile 可以被跳过;如果判断“可能存在”,读路径还要继续走 HFile index 和 block 读取。假阳性会让读路径多读一个文件,但不会让 HBase 返回不存在的 Cell。
最小实验
UNVERIFIED_RUNTIME:当前任务环境未安装 HBase 2.6.6、Hadoop 3.4.3,也没有 JDK 17 运行线。以下步骤是目标环境中的复现实验,不包含伪造输出、耗时、命中率或日志。
复现实验从一张开启 row 级 Bloom Filter、保留默认 block cache 行为的表开始,再写入几个离散 row key,flush 后分别读取存在和不存在的行,用 RegionServer Web UI、JMX 或 metrics 观察 block cache、Bloom 和 StoreFile 相关计数。命令只给出操作序列,不附带未采集的输出。
1 | |
1 | |
1 | |
写入三条 row key 后触发 flush:
1 | |
1 | |
1 | |
1 | |
随后分别执行存在和不存在 row 的 get:
1 | |
1 | |
观察 RegionServer Web UI、JMX 或 metrics 中与 block cache、Bloom、StoreFile 相关的计数。实验只应记录字段含义和变化方向,不应从单机小样本推导生产延迟。
使用 Java Public API 时,Get 可以显式控制读取列和 cache block 行为:
1 | |
UNVERIFIED_RUNTIME:这段代码只使用 HBase 2.6.x Public API;本轮未在目标依赖组合下编译运行。
清理实验表:
1 | |
1 | |
1 | |
失败恢复路径
Region 移动、split 或 RegionServer 失败时,客户端缓存的 Region 位置可能变旧。客户端收到异常后会刷新定位,再通过 hbase:meta 或注册表路径获得新的 RegionServer 地址。这个过程不等于每次 Get 都查 hbase:meta;正常路径依赖缓存,异常路径才重定位。
BlockCache 不是恢复机制。RegionServer 重启后,堆内或堆外缓存状态可能丢失,读请求会重新从 HFile 和 HDFS 恢复热度。WAL 恢复决定未 flush 写入能否回放,BlockCache 只影响读延迟。
工程迁移
| HBase 机制 | 可迁移模式 | 其他系统中的对应问题 |
|---|---|---|
| Bloom Filter | 概率结构换无效 I/O | LSM 数据库的 SSTable 跳过、缓存穿透防护 |
| BlockCache | 热块驻留 | 搜索引擎 segment cache、数据库 buffer pool |
| HFile index | 有序文件内定位 | SSTable sparse index、列存 row group index |
| MemStore + HFile merge | 多层视图归并 | LSM memtable 与 SSTable 读合并 |
模式公式可以写成:read(row) = merge(memory_view, cached_blocks, indexed_files) - invisible_versions。听到“文件很多但单点读要短”的问题,首先检查负向过滤、块缓存和文件数量,而不是直接提高线程数。
常见误解
| 误解 | 更准确的说法 |
|---|---|
| BlockCache 命中就没有读放大 | 命中减少 HDFS 读取,但多 StoreFile 仍有 scanner 和归并成本 |
| Bloom Filter 能证明数据存在 | Bloom 只能可靠证明“不存在”,存在判断可能是假阳性 |
Get 每次都查 hbase:meta |
客户端缓存 Region 位置,异常或缓存失效时才重新定位 |
| HBase 读路径就是 HDFS 随机读 | HBase 在 HDFS 文件之上增加 HFile 索引、缓存和合并语义 |
| 大 scan 应该默认缓存所有 block | 一次性 scan 常需要关闭 cache blocks,避免污染热点缓存 |
练习
- 对同一张表分别设置
BLOOMFILTER => 'ROW'与BLOOMFILTER => 'ROWCOL',比较适合的查询形态。 - 对一次大范围
Scan使用setCacheBlocks(false),观察热点Get的缓存命中是否受影响。 - 在 flush 多次之后执行同一个
Get,观察 StoreFile 数量变化和读路径指标变化。 - 把冷热字段拆到不同 column family,再对比只读热字段时参与的 Store 数量。
系列导航
参考资料
- Apache HBase Reference Guide:Architecture、RegionServer、BlockCache、Bloom Filter、HFile Format:https://hbase.apache.org/docs/
- Apache HBase 2.6 User API:
Get、Scan、Table、Connection:https://hbase.apache.org/2.6/apidocs/ - Apache HBase 2.6 Developer API:
BlockCache、HFile 相关接口:https://hbase.apache.org/2.6/devapidocs/ - Apache HBase 源码 rel/2.6.6:
HRegion、StoreFileScanner、HFileBlock、HFileReaderImpl:https://github.com/apache/hbase/tree/rel/2.6.6 - Apache Hadoop 3.4.3 HDFS 文档:https://hadoop.apache.org/docs/r3.4.3/
