一篇文章搞懂 HBase 的 BlockCache

前言

本文隶属于专栏《大数据技术体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和参考文献请见大数据技术体系


BlockCache

众所周知,提升数据库读取性能的一个核心方法是,尽可能将热点数据存储到内存中,以避免昂贵的 IO 开销。

现代系统架构中,诸如 Redis 这类缓存组件已经是体系中的核心组件,通常将其部署在数据库的上层,拦截系统的大部分请求,保证数据库的“安全”,提升整个系统的读取效率。

同样为了提升读取性能, HBase 也实现了一种读缓存结构一BlockCache

客户端读取某个 Block ,首先会检查该 Block 是否存在于 BlockCache ,如果存在就直接加载出来,如果不存在则去 HFile 文件中加载,加载出来之后放到 BlockCache 中,后续同一请求或者邻近数据查找请求可以直接从内存中获取,以避免昂贵的 IO 操作。

从字面意思可以看出来, BlockCache 主要用来缓存 Block。

需要关注的是, Block 是 HBase 中最小的数据读取单元,即数据从 HFile 中读取都是以 Block 为最小单元执行的。

BlockCache 是 RegionServer 级别的,一个 RegionServer 只有一个 BlockCache ,在 RegionServer 启动时完成 BlockCache 的初始化工作。

HBase 的架构图如下所示。

在这里插入图片描述

BlockCache 名称中的 Block 指的是 HBase 的 Block,目前有很多种 Block:

  • DATA
  • ENCODED_DATA
  • LEAF_INDEX
  • BLOOM_CHUNK
  • META
  • INTERMEDIATE_INDEX
  • RO0T_INDEX
  • FILE_INFO
  • GENERAL_BLOOM_META
  • DELETE_FAMILY_BLOOM_META
  • TRAILER
  • INDEX_V1

到目前为止, HBase 先后实现了 3 种 BlockCache 方案

  • LRUBlockCache 是最早的实现方案,也是默认的实现方案;
  • HBase 0.92 版本实现了第二种方案 SlabCache ,参见 HBase-4027
  • HBase 0.96 之后官方提供了另一种可选方案 BucketCache ,参见 HBase-7404

这 3 种方案的不同之处主要在于内存管理模式,其中 LRUBlockCache 是将所有数据都放入 JVMHeap 中,交给 JVM 进行管理。

而后两种方案采用的机制允许将部分数据存储在堆外。

这种演变本质上是因为 LRUBlockCache 方案中 JVM 垃圾回收机制经常导致程序长时间暂停,而采用堆外内存对数据进行管理可以有效缓解系统长时间 GC 。


BlockCache 默认是开启的
你不需要做额外的事情去开启 BlockCache。 如果你想让某个列族不使用 BlockCache ,可以通过以下命令关闭它:

hbase>alter 'myTable',CONFIGURATION=>(NAME=>'myCF',BLOCKCACHE=>'false'} 

LRUBlockCache

首当其冲的肯定就是完全基于 JVM heap 的 LRU 方案了。

在 0.92 版本之前只有这种 BlockCache 的实现方案。

LRU 就是 LeastRecentlyUsed,即近期最少使用算法的缩写。

读出来的 block 会被放到 BlockCache 中待下次查询使用。

当缓存满了的时候,会根据 LRU 的算法来淘汰 block。

LRUBlockCache 被分为三个区域,如下表所示。

area名称占用比例说明
single-access25%单次读取区(single-access)。block被读出后先放到这个区域,当被读到多次后会升级到下一个区域
multi-access50%多次读取区。当一个被缓存到单次读取区(single-access)后又被多次访问, 会升级到这个区
in-memory25%这个区域跟block被访问了几次没有关系,它只存放哪些被设置了IN-MEMORY=true的列族中读取出来的block

看起来是不是很像 JVM 的新生代、年老代、永久代?没错,这个方案就是模拟JVM 的分代设计而做的。


列族的 IN-MEMORY 属性是干什么的

列族被设置为 IN-MEMORY 并不是意味着这个列族是存储在内存中的。 这个列族依然是跟别的列族一样存储在硬盘上。

一般的 Block 被第一次读出后是放到 single-access 的,只有当被访问多次后才会放到 multi-access,而带有 IN-MEMORY 属性的列族中的 Block 一开始就被放到 in-memory 区域。

这个区域的缓存有最高的存活时间,在需要淘汰 Block 的时候,这个区域的 Block 是最后被考虑到的,所以这个属性仅仅是为了 BlockCache 而创造的。


LRUBlockCache 的相关参数

目前 BlockCache 的堆内内存方案就只有 LRUBlockCache,而且你还关不掉它,只能调整它的大小。相关参数为:

参数说明默认值
hfile.block.cache.sizeLRUBlockCache 占用的内存比例0.4

BlockCache 配置和 Memstore 配置的联动影响

设置 hfile.block.cache.size 的时候要注意在 HBase 的内存使用上有一个规则那就是 Memstore + BlockCache 的内存占用比例不能超过 0.8 (即 80 %),否则就要报错。 因为必须要留 20 %作为机动空间。 用配置项来说明就是:

hbase.regionserver.global.memstore.size + hfile.block.cache.size <= 0.8

值得一提的是,这两个配置项的默认值都是 0.4,也就是说默认项的总和就己经达到了他们俩可以占用的内存比例上限了,所以基本没事就不用去加大这两个配置项,你调大哪一个,都必须相应地调小另外一个。

的确 BlockCache 可以带来很多好处,就是一个菜鸟都可以想到用内存来做缓存提高读取性能,但是 LRUBlockCache 有什么坏处呢?

完全基于 JVM Heap 的缓存,势必带来一个后果:随着内存中对象越来越多,每隔一段时间都会引发一次 Full GC。在 Full GC 的过程中,整个 JVM 完全处于停滞状态,有的时候长达几分钟。


SlabCache

为了解决 LRUBlockCache 方案中因 JVM 垃圾回收导致的服务中断问题, SlabCache 方案提出使用 Java NIO DirectByteBuffer 技术实现堆外内存存储,不再由 JVM 管理数据内存。

默认情况下,系统在初始化的时候会分配两个缓存区,分别占整个 BlockCache 大小的 80 %和 20 %,每个缓存区分别存储固定大小的 B1ock ,其中前者主要存储小于等于 64K 的 Block ,后者存储小于等于 128K 的 Block ,如果一个 Block 太大就会导致两个区都无法缓存。

和 LRUBlockCache 相同, SlabCache 也使用 Least-Recently-Used 算法淘汰过期的 Block。

和 LRUBlockCache 不同的是, SlabCache 淘汰 Block 时只需要将对应的 BufferByte 标记为空闲,后续 cache 对其上的内存直接进行覆盖即可。

线上集群环境中,不同表不同列簇设置的 BlockSize 都可能不同,很显然,默认只能存储小于等于 128KB Block 的 SlabCache 方案不能满足部分用户场景。

比如,用户设置 BlockSize = 256K ,简单使用 SlabCache 方案就不能达到缓存这部分 Block 的目的。

因此 HBase 在实际实现中将 SlabCache 和 LRUBlockCache 搭配使用,称为 DoubleBlockCache

在一次随机读中,一个 Block 从 HDFS 中加载出来之后会在两个 Cache 中分别存储一份。

缓存读时首先在 LRUBlockCache 中查找,如果 Cache Miss 再在 SlabCache 中查找,此时如果命中,则将该 Block 放入 LRUBlockCache 中。

经过实际测试, DoubleBlockCache 方案有很多弊端。

比如, SlabCache 中固定大小内存设置会导致实际内存使用率比较低,而且使用 LRU BlockCache 缓存 Block 依然会因为 JVM GC 产生大量内存碎片

因此在 HBase 0.98 版本之后,已经不建议使用该方案。


BucketCache

BucketCache 借鉴了 SlabCache 的创意,也用上了堆外内存。

不过它是这么用的:相比起只有 2 个区域的 SlabeCache, BucketCache 一上来就分配了 14 种区域。

这 14 种区域分别放的是大小为 4KB 、 8KB 、 16KB 、 32KB 、 40KB 、 48KB 、 56KB 、 64KB 、 96KB 、 128KB 、 192KB 、 256KB 、 384KB 、 512KB 的 B1ock 。

而且这个种类列表还是可以手动通过设置 hbase.bucketcache.bucket.sizes 属性来定义,这 14 种类型可以分配出很多个 Bucket。

BucketCache 的存储不一定要使用堆外内存,是可以自由在 3 种存储介质直接选择:堆( heap )、堆外( offheap )、文件( file )。

通过设置 hbase.bucketcache.ioengine 为 heap 、 offheap 或者 file 来配置。

每个 Bucket 的大小上限为最大尺寸的 block * 4 ,比如可以容纳的最大的 Block 类型是 512KB ,那么每个 Bucket 的大小就是 512KB * 4 = 2048KB 。

系统一启动 BucketCache 就会把可用的存储空间按照每个 Bucket 的大小上限均分为多个 Bucket 。 如果划分完的数量比你的种类还少,比如比 14 (默认的种类数量)少,就会直接报错,因为每一种类型的 Bucket 至少要有一个 Bucket。

BucketCache 实现起来的样子就像如下图所示(每个区域的大小都是 512 * 4 )。

在这里插入图片描述


BucketCache 对内存地址的管理

BucketCache 还有一个特别的长处,那就是它自己来划分内存空间、自己来管理内存空间, Block 放进去的时候是考虑到 offset 偏移量的,所以内存碎片少,发生 GC 的时间很短。


为什么存储介质有 file ?

我们用缓存不就是为了使用内存,然后利用内存比硬盘快得优势来提高读写的性能吗?

大家不要忘记了还有 SSD 硬盘,最开始设计这种策略的初衷就是想把 SSD 作为一层比传统机械硬盘更快的缓存层来使用,所以你可以把 file 这种类型等同于 SSD-file。

使用 BucketCache 有以下的好处:

  • 这是第一个可以使用 SSD 硬盘的缓存策略,这是最大的亮点。
  • 这种策略极大地改进了 SlabCache 使用率低的问题。
  • 配置极其灵活,可以适用于多种场景。

在实际测试中也表现出了很高的性能,所以 HBase 就顺理成章地把 SlabCache 废弃了。


BucketCache 用到的配置项

BucketCache 默认也是开启的。

如果你想让某个列族不使用 BucketCache ,你可以使用以下命令:

hbase>alter 'myTable',CONFIGURATION=>{CACHE_DATA_IN_L1=>'true'}}

意思是数据只缓存在一级缓存( LRUCache )中,不使用二级缓存( BucketCache )。


BucketCache 相关配置项

配置项说明默认值
hbase.bucketcache.ioengine使用的存储介质,可选值为 heap、offheap、fileoffheap
hbase.bucketcache.combinedcache.enabled是否打开组合模式( CombinedBlockCache )true
hbase.bucketcache.sizeBucketCache 所占的大小,如果设置为 0.0~1.0 ,则代表了占堆内存的百分比。如果是大于 1 的值,则代表实际的 BucketCache 的大小,单位为 MB。 为什么要一个参数两用这么别扭呢?因为 BucketCache 既可以用于堆内存,还可以用于堆外内存和硬盘。默认值为 0.0 ,即关闭 BucketCache
hbase.bucketcache.bucket.sizes定义所有 Block 种类,默认为 14 种,种类之间用逗号分隔。 单位为 B ,每一种类型必须是 1024 的整数倍,否则会报异常: java.io.I0Exception: Invalid HFile block magic4 、 8 、 16 、 32 、 40 、 48 、 56 、 64 、 96 、 128 、 192 、 256 、 384 、 512 。
-XX:MaxDirectMemorySize这个参数不是在 hbase-site.xml 中配置的,而是 JVM 启动的参数。 如果你不配置这个参数, JVM 会按需索取堆外内存;如果你配置了这个参数,你可以定义 VM 可以获得的堆外内存上限。 显而易见的,这个参数值必须比 hbase.bucketcache.size 大。

以前还有一个 hbase.bucketcache.combinedcache.percentage 配置项,用于配置 BucketCache 在组合模式中的百分比,后来被改名为 hbase.bucketcache.percentage.in.combinedcache ,最后被废弃了。原因是这个参数太难让人理解了,直接用 hbase.bucketcache.size 就行了。


组合模式

前面说了 BucketCache 那么多好处,那么是不是 BucketCache 就完爆 LRUCache 了?

答案是没有,在很多情况下倒是 LRUCache 完爆 BucketCache 。

虽然后面有了 SlabCache 和 BucketCache ,但是这些 Cache 从速度和可管理性上始终无法跟完全基于内存的 LRUCache 相媲美。

虽然 LRUCache 有严重的 Full GC 问题, HBase 一直都没有放弃 LRUCache。

所以,还是那句话,不要不经过测试比较就直接换策略。

在 SlabCache 的时代, SlabCache ,是跟 LRUCache 一起使用的,每一个 Block 被加载出来都是缓存两份,一份在 SlabCache 一份在 LRUCache ,这种模式称之为 DoubleBlockCache。

读取的时候 LRUCache 作为 L1 层缓存(一级缓存),把 SlabCache 作为 L2 层缓存(二级缓存)。

在 BucketCache 的时代,也不是单纯地使用 BucketCache ,但是这不是一二级缓存的结合,而是另一种模式,叫组合模式( Combined BlockCahce )。

具体地说就是把不同类型的 Block 分别放到 LRUCache 和 BucketCache 中。

IndexBlock 和 BloomBlock 会被放到 LRUCache 中。

DataBlock 被直接放到 BucketCache 中,所以数据会去 LRUCache 查询一下,然后再去 BucketCache 中查询真正的数据。

其实这种实现是一种更合理的二级缓存,数据从一级缓存到二级缓存最后到硬盘,数据是从小到大,存储介质也是由快到慢。

考虑到成本和性能的组合,比较合理的介质是:

LRUCache 使用内存-> BuckectCache 使用 SSD -> HFile 使用机械硬盘

关于 LRUBlockCache 和 BucketCache 单独使用谁比较强,曾经有人做过一个测试,并写了一篇报告出来,标题为 Comparing BlockCache Deploys ,结论是:

  • 因为 BucketCache 自己控制内存空间,碎片比较少,所以 GC 时间大部分都比 LRUCache 短。
  • 在缓存全部命中的情况下, LRUCache 的吞土量是 BucketCache 的两倍;在缓存基本命中的情况下, LRUCache 的吞吐量跟 BucketCache 基本相等。
  • 读写延迟, I0 方面两者基本相等。
  • 缓存全部命中的情况下, LRUCache 比使用 file 模式的 BucketCacheCPU 占用率低一倍,但是跟其他情况下差不多。

从整体上说 LRUCache 的性能好于 BucketCache ,但由于 Full GC 的存在,在某些时刻 JVM 会停止响应,造成服务不可用。

所以适当的搭配 BucketCache 可以缓解这个问题。

如何看缓存命中率?
看缓存的命中率,只需要打开 hadoop metrics ,查看 hbase.regionserver.blockCacheHitRatio 。该值的取值范围为 0.0~1.0 。


BucketCache 中 Block 缓存写入、读取流程

下图所示是 Block 写入缓存以及从缓存中读取 Block 的流程,图中主要包括 5 个模块:

在这里插入图片描述

  • RAMCache 是一个存储 blockKey 和 Block 对应关系的 HashMap
  • WriteThead 是整个 Block 写入的中心枢纽,主要负责异步地将 Block 写入到内存空间。
  • BucketAllocator 主要实现对 Bucket 的组织管理,为 Block 分配内存空间。
  • IOEngine 是具体的内存管理模块,将 Block 数据写入对应地址的内存空间。
  • BackingMap 也是一个 HashMap ,用来存储 blockKey 与对应物理内存偏移量的映射关系,并且根据 blockKey 定位具体的 Block。图中实线表示 Block 写入流程,虚线表示 Block 缓存读取流程。

Block 缓存写入流程

  1. 将 Block 写入 RAMCache。 实际实现中, HBase 设置了多个 RAMCache ,系统首先会根据 blockKey 进行 hash ,根据 hash 结果将 Block 分配到对应的 RAMCache 中。
  2. WriteThead 从 RAMCache 中取出所有的 Block。和 RAMCache 相同, HBase 会同时启动多个 WriteThead 并发地执行异步写入,每个 WriteThead 对应一个 RAMCache
  3. 每个 WriteThead 会遍历 RAMCache 中所有 Block ,分别调用 bucketAllocator 为这些 Block 分配内存空间。
  4. BucketAllocator 会选择与 Block 大小对应的 Bucket 进行存放,并且返回对应的物理地址偏移量 offset 。
  5. WriteThead 将 Block 以及分配好的物理地址偏移量传给 IOEngine 模块,执行具体的内存写入操作。
  6. 写入成功后,将 blockKey 与对应物理内存偏移量的映射关系写人 BackingMap 中,方便后续查找时根据 blockKey 直接定位。

Block 缓存读取流程

  1. 首先从 RAMCache 中查找。对于还没有来得及写入 Bucket 的缓存 Block ,一定存储在 RAMCache 中。
  2. 如果在 RAMCache 中没有找到,再根据 blockKey 在 BackingMap 中找到对应的物理偏移地址量 offset 。
  3. 根据物理偏移地址 offset 直接从内存中查找对应的 Block 数据。
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值