前置问题与边界

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

写入三条 row key 后触发 flush:

1
put 'lab:t05_read_path', 'user#0001', 'cf:name', 'alice'
1
put 'lab:t05_read_path', 'user#0002', 'cf:name', 'bob'
1
put 'lab:t05_read_path', 'user#0999', 'cf:name', 'zoe'
1
flush 'lab:t05_read_path'

随后分别执行存在和不存在 row 的 get

1
get 'lab:t05_read_path', 'user#0001'
1
get 'lab:t05_read_path', 'user#4040'

观察 RegionServer Web UI、JMX 或 metrics 中与 block cache、Bloom、StoreFile 相关的计数。实验只应记录字段含义和变化方向,不应从单机小样本推导生产延迟。

使用 Java Public API 时,Get 可以显式控制读取列和 cache block 行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 HBaseGetExample {
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:t05_read_path"))) {
Get get = new Get(Bytes.toBytes("user#0001"));
get.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("name"));
get.setCacheBlocks(true);
Result result = table.get(get);
byte[] value = result.getValue(Bytes.toBytes("cf"), Bytes.toBytes("name"));
System.out.println(Bytes.toString(value));
}
}
}

UNVERIFIED_RUNTIME:这段代码只使用 HBase 2.6.x Public API;本轮未在目标依赖组合下编译运行。

清理实验表:

1
disable 'lab:t05_read_path'
1
drop 'lab:t05_read_path'
1
drop_namespace 'lab'

失败恢复路径

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,避免污染热点缓存

练习

  1. 对同一张表分别设置 BLOOMFILTER => 'ROW'BLOOMFILTER => 'ROWCOL',比较适合的查询形态。
  2. 对一次大范围 Scan 使用 setCacheBlocks(false),观察热点 Get 的缓存命中是否受影响。
  3. 在 flush 多次之后执行同一个 Get,观察 StoreFile 数量变化和读路径指标变化。
  4. 把冷热字段拆到不同 column family,再对比只读热字段时参与的 Store 数量。

系列导航

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

参考资料