HBase 作为面向列的分布式数据库,读取链路中最昂贵的部分往往是磁盘 IO。为了缓解这一问题,HBase 设计了 block cache 机制,将频繁访问的 HFile 数据块缓存在内存中。当客户端发起 Get 或 Scan 请求时,RegionServer 会先检查目标数据块是否已在缓存里,命中则直接返回,未命中才走 HDFS 读盘并填入缓存。理解这套机制的底层行为和参数边界,是做稳定高并发读取的前提。

一、block cache 的基本组成与分类
HBase 的数据文件 HFile 由多个固定大小的 block 组成,常见块类型包括 DATA_BLOCK(实际数据)、INDEX_BLOCK(索引)、BLOOM_BLOCK(布隆过滤器)以及 META_BLOCK。block cache 就是这些 block 解压后在内存中的副本容器。不同块被访问的模式差异很大,例如索引块几乎每次读都要用,而用户数据块取决于查询热点。
在默认实现中,HBase 使用 LruBlockCache 作为堆内缓存管理器。它并没有把所有块混在一起淘汰,而是按访问特征划分为三个区段:single-access(单次读)、multi-access(多次读)和 in-memory(常驻,如索引和布隆块)。新块默认进 single 区,被再次访问后升级到 multi 区,而通过列族属性配置为 in-memory 的块则直接放进优先级最高的区域,减少被误淘汰的概率。
1.1 分段 LRU 的配额比例
LruBlockCache 默认将堆内分配给 block cache 的总容量按 25%、50%、25% 划分给 single、multi、in-memory 三个区。这种划分避免了一次性全表扫描把索引块冲掉的问题。例如执行大范围 Scan 时产生的大量 single 区数据,即使堆满也只会挤占自己的配额,不会直接影响 multi 区里的真实热点数据。
需要注意的是,in-memory 区的 25% 是逻辑上限而非预留。如果系统没有配置任何 in-memory 列族,这部分空间可以被其他区借用,但一旦有 in-memory 块需要写入且空间不足,就会触发对应区域的淘汰。这也解释了为什么开启布隆过滤和索引缓存后,随机读延迟会更平稳。
二、核心配置参数与调优思路
控制 block cache 最关键的是 hfile.block.cache.size,它表示 RegionServer 堆内存中分配给 LruBlockCache 的比例,默认 0.4。另一个大头是 MemStore 用的 hbase.regionserver.global.memstore.size,默认也是 0.4。两者之和不能超过 0.8,否则 RegionServer 启动就会报错,因为还要留内存给 RPC 栈和 JVM 自身。
调优时不能只看命中率绝对值。如果业务以写为主、少量随机读,把 block cache 调到 0.6 反而可能挤压 MemStore 导致频繁 flush 和 compaction,加重 IO。反过来,纯读型业务可以把 MemStore 压到 0.2,block cache 开到 0.6 甚至更高。下面是一段 hbase-site.xml 的示例片段:
<configuration>
<property>
<name>hfile.block.cache.size</name>
<value>0.5</value>
<description>堆内存中 block cache 占用比例,读多写少可上调</description>
</property>
<property>
<name>hbase.regionserver.global.memstore.size</name>
<value>0.3</value>
<description>MemStore 总比例,需与 block cache 之和小于 0.8</description>
</property>
</configuration>
2.1 列族级别的 in-memory 设置
对于极热的小表,比如字典表、配置表,可以在建表时把列族设为 BLOCKCACHE 开启且 IN_MEMORY 为 true。这样对应的数据块会优先停留在 in-memory 区,几乎不被 LRU 淘汰。但请注意 IN_MEMORY 不等于强制常驻,它只是提高优先级,极端内存压力下仍可能被回收。
错误做法是给所有列族都开 IN_MEMORY,这会让分段策略失效,缓存区被普通数据占满,反而害了真正的索引块。正确方式是用 HBase Shell 或 Java API 精确控制:
create 'dict_table', {NAME => 'cf', BLOCKCACHE => 'true', IN_MEMORY => 'true'}
三、堆外缓存 BucketCache 的引入
当堆内存扩大到几十 GB 时,LruBlockCache 带来的 GC 停顿会成为瓶颈。HBase 提供 BucketCache 方案,把 DATA_BLOCK 放到堆外内存或本地文件、SSD 上,只让索引和布隆块留在堆内 LruBlockCache。组合模式称为 CombinedBlockCache,通过 hbase.bucketcache.ioengine 指定介质。
以下配置演示了使用堆外内存作为 BucketCache,并组合 LruBlockCache 的方式。这种方式能显著降低 Full GC 频率,适合大堆场景。代码中的参数表示每个 bucket 为 64MB,总共 1024 个,即约 64GB 堆外空间。
<property> <name>hbase.bucketcache.ioengine</name> <value>offheap</value> </property> <property> <name>hbase.bucketcache.size</name> <value>65536</value> </property> <property> <name>hbase.bucketcache.combinedcache.enabled</name> <value>true</value> </property>
3.1 不同 IO 引擎的取舍
offheap 模式利用 DirectByteBuffer,不受老年代 GC 扫描影响,但受系统直接内存上限约束,需在 JVM 启动加 -XX:MaxDirectMemorySize。file 模式把块写本地磁盘,适合内存不足但想缓存元数据的场景,代价是读缓存块多一次本地 IO。ssd 模式则平衡成本和延迟,是混合存储机器的常见选择。
生产环境监控时,应重点看 RegionServer Web UI 的 blockCacheHitRatio。若 Combined 模式下 DATA 命中低但 INDEX 命中高,说明堆外容量不足或访问过于离散;若整体命中低于 0.1,往往要重新审视表设计,比如 RowKey 是否导致热点分散,而非一味加内存。
四、常见误区与诊断方法
不少人在遇到读延迟高时,第一反应是调大 hfile.block.cache.size,却忽略 block 大小本身的影响。HFile 的 BLOCKSIZE 默认 64KB,如果单行数据很大且每次读只取一列,小 block 更省缓存;若是宽行全列扫描,大 block 减少索引次数。这个参数和 cache 是联动的,不能孤立看待。
另一个误区是认为 block cache 能替代布隆过滤器。实际上布隆块常驻 in-memory 区,先帮你在 HFile 层级判断某 RowKey 是否存在,避免无效读盘和填塞 DATA 缓存。关闭布隆会让 block cache 装入大量无用块,命中率虚低。诊断时可用 hbase hfile 命令抽样分析块分布,或用 Metrics 接口拉取 cache 各区段大小。
hbase hfile -m -f /hbase/data/default/test/xxx/cf/xxxx -c
通过上述命令可以查看具体 HFile 的块数量、压缩比和平均块大小,辅助判断是否需要调整 BLOCKSIZE 或压缩算法。压缩虽不直接归 block cache 管,但 Snappy 或 ZSTD 解压后入 cache,能变相提升同等内存下的有效命中数据量。
五、总结性实践建议
面对随机读敏感的业务,先通过压测观察 blockCacheHitRatio 与 GC 时间,再决定堆内堆外分工。读多写少且堆小于 16GB 的节点,用默认 LruBlockCache 并把 size 开到 0.5 通常够用;大堆读服务型集群,尽早切到 BucketCache 组合模式。把 IN_MEMORY 留给真正的元数据列族,普通数据靠访问局部性自然沉淀到 multi 区。
最后要强调,block cache 调优不是一次性的。随着数据量膨胀和访问模式迁移,旧的热点可能变冷,缓存命中曲线会漂移。把缓存相关 Metrics 接进监控系统,设置命中率下跌告警,才能在业务感知变慢前完成参数迭代。
HBaseblock_cacheLruBlockCache修改时间:2026-08-10 15:52:13