乐趣区

关于hbase:HBase原理RegionServer核心组件之BlockCache

家喻户晓,晋升数据库读取性能的一个外围办法是,尽可能将热点数据存储到内存中,以防止低廉的 IO 开销。古代零碎架构中,诸如 Redis 这类缓存组件曾经是体系中的外围组件,通常将其部署在数据库的下层,拦挡零碎的大部分申请,保障数据库的“平安”,晋升整个零碎的读取效率。

同样为了晋升读取性能,HBase 也实现了一种读缓存构造——BlockCache。客户端读取某个 Block,首先会查看该 Block 是否存在于 Block Cache,如果存在就间接加载进去,如果不存在则去 HFile 文件中加载,加载进去之后放到 Block Cache 中,后续同一申请或者邻近数据查找申请能够间接从内存中获取,以防止低廉的 IO 操作。

从字面意思能够看进去,BlockCache 次要用来缓存 Block。须要关注的是,Block 是 HBase 中最小的数据读取单元,即数据从 HFile 中读取都是以 Block 为最小单元执行的。

BlockCache 是 RegionServer 级别的,一个 RegionServer 只有一个 BlockCache,在 RegionServer 启动时实现 BlockCache 的初始化工作。到目前为止,HBase 先后实现了 3 种 BlockCache 计划,LRUBlockCache 是最早的实现计划,也是默认的实现计划;HBase 0.92 版本实现了第二种计划 SlabCache,参见 HBASE-4027;HBase 0.96 之后官网提供了另一种可选计划 BucketCache,参见 HBASE-7404。

这 3 种计划的不同之处次要在于内存管理模式,其中 LRUBlockCache 是将所有数据都放入 JVM Heap 中,交给 JVM 进行治理。而后两种计划采纳的机制容许将局部数据存储在堆外。这种演变实质上是因为 LRUBlockCache 计划中 JVM 垃圾回收机制常常导致程序长时间暂停,而采纳堆外内存对数据进行治理能够无效缓解零碎长时间 GC。

LRUBlockCache

LRUBlockCache 是 HBase 目前默认的 BlockCache 机制,实现绝对比较简单。它应用一个 ConcurrentHashMap 治理 BlockKey 到 Block 的映射关系,缓存 Block 只须要将 BlockKey 和对应的 Block 放入该 HashMap 中,查问缓存就依据 BlockKey 从 HashMap 中获取即可。同时,该计划采纳严格的 LRU 淘汰算法,当 Block Cache 总量达到肯定阈值之后就会启动淘汰机制,最近起码应用的 Block 会被置换进去。在具体的实现细节方面,须要关注以下三点。

1. 缓存分层策略

HBase 采纳了缓存分层设计,将整个 BlockCache 分为三个局部:single-access、multi-access 和 in-memory,别离占到整个 BlockCache 大小的 25%、50%、25%。

在一次随机读中,一个 Block 从 HDFS 中加载进去之后首先放入 single-access 区,后续如果有屡次申请拜访到这个 Block,就会将这个 Block 移到 multi-access 区。而 in-memory 区示意数据能够常驻内存,个别用来寄存拜访频繁、量小的数据,比方元数据,用户能够在建表的时候设置列簇属性 IN_MEMORY=true,设置之后该列簇的 Block 在从磁盘中加载进去之后会间接放入 in-memory 区。

须要留神的是,设置 IN_MEMORY=true 并不意味着数据在写入时就会被放到 in-memory 区,而是和其余 BlockCache 区一样,只有从磁盘中加载出 Block 之后才会放入该区。另外,进入 in-memory 区的 Block 并不意味着会始终存在于该区,仍会基于 LRU 淘汰算法在空间有余的状况下淘汰最近最不沉闷的一些 Block。

因为 HBase 零碎元数据(hbase:meta,hbase:namespace 等表)都寄存在 in-memory 区,因而对于很多业务表来说,设置数据属性 IN_MEMORY=true 时须要十分审慎,肯定要确保此列簇数据量很小且拜访频繁,否则可能会将 hbase:meta 等元数据挤出内存,重大影响所有业务性能。

2. LRU 淘汰算法实现
在每次 cache block 时,零碎将 BlockKey 和 Block 放入 HashMap 后都会查看 BlockCache 总量是否达到阈值,如果达到阈值,就会唤醒淘汰线程对 Map 中的 Block 进行淘汰。零碎设置 3 个 MinMaxPriorityQueue,别离对应上述 3 个分层,每个队列中的元素依照最近起码被应用的规定排列,零碎会优先取出最近起码应用的 Block,将其对应的内存开释。可见,3 个分层中的 Block 会别离执行 LRU 淘汰算法进行淘汰。

3. LRUBlockCache 计划优缺点
LRUBlockCache 计划应用 JVM 提供的 HashMap 治理缓存,简略无效。但随着数据从 single-access 区降职到 multi-access 区或长时间停留在 single-access 区,对应的内存对象会从 young 区降职到 old 区,降职到 old 区的 Block 被淘汰后会变为内存垃圾,最终由 CMS 回收(Conccurent Mark Sweep,一种标记革除算法),显然这种算法会带来大量的内存碎片,碎片空间始终累计就会产生臭名远扬的 FullGC。尤其在大内存条件下,一次 Full GC 很可能会继续较长时间,甚至达到分钟级别。Full GC 会将整个过程暂停,称为 stop-the-world 暂停(STW),因而长时间 Full GC 必然会极大影响业务的失常读写申请。正因为该计划有这样的弊病,之后相继呈现了 SlabCache 计划和 BucketCache 计划。

SlabCache

为了解决 LRUBlockCache 计划中因 JVM 垃圾回收导致的服务中断问题,SlabCache 计划提出应用 Java NIO DirectByteBuffer 技术实现堆外内存存储,不再由 JVM 治理数据内存。默认状况下,零碎在初始化的时候会调配两个缓存区,别离占整个 BlockCache 大小的 80% 和 20%,每个缓存区别离存储固定大小的 Block,其中前者次要存储小于等于 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 中查找,如果 CacheMiss 再在 SlabCache 中查找,此时如果命中,则将该 Block 放入 LRUBlockCache 中。

通过理论测试,DoubleBlockCache 计划有很多弊病。比方,SlabCache 中固定大小内存设置会导致理论内存使用率比拟低,而且应用 LRUBlockCache 缓存 Block 仍然会因为 JVM GC 产生大量内存碎片。因而在 HBase 0.98 版本之后,曾经不倡议应用该计划。

BucketCache

SlabCache 计划在理论利用中并没有很大水平改善原有 LRUBlockCache 计划的 GC 弊病,还额定引入了诸如堆外内存使用率低的缺点。然而它的设计并不是一无是处,至多在应用堆外内存这方面给予了后续开发者很多启发。站在 SlabCache 的肩膀上,社区工程师设计开发了另一种十分高效的缓存计划——BucketCache。

BucketCache 通过不同配置形式能够工作在三种模式下:heap,offheap 和 f ile。heap 模式示意这些 Bucket 是从 JVM Heap 中申请的;offheap 模式应用 DirectByteBuffer 技术实现堆外内存存储管理;f ile 模式应用相似 SSD 的存储介质来缓存 Data Block。无论工作在哪种模式下,BucketCache 都会申请许多带有固定大小标签的 Bucket,和 SlabCache 一样,一种 Bucket 存储一种指定 BlockSize 的 Data Block,但和 SlabCache 不同的是,BucketCache 会在初始化的时候申请 14 种不同大小的 Bucket,而且如果某一种 Bucket 空间有余,零碎会从其余 Bucket 空间借用内存应用,因而不会呈现内存使用率低的状况。

理论实现中,HBase 将 BucketCache 和 LRUBlockCache 搭配应用,称为 CombinedBlock-Cache。和 DoubleBlockCache 不同,零碎在 LRUBlockCache 中次要存储 Index Block 和 Bloom Block,而将 Data Block 存储在 BucketCache 中。因而一次随机读须要先在 LRUBlockCache 中查到对应的 Index Block,而后再到 BucketCache 查找对应 Data Block。BucketCache 通过更加正当的设计修改了 SlabCache 的弊病,极大升高了 JVM GC 对业务申请的理论影响,但其也存在一些问题。比方,应用堆外内存会存在拷贝内存的问题,在肯定水平上会影响读写性能。当然,在之后的 2.0 版本中这个问题失去了解决,参见 HBASE-11425。

相比 LRUBlockCache,BucketCache 实现绝对比较复杂。它没有应用 JVM 内存治理算法来治理缓存,而是本人对内存进行治理,因而大大降低了因为呈现大量内存碎片导致 Full GC 产生的危险。鉴于生产线上 CombinedBlockCache 计划应用的普遍性,下文次要介绍 BucketCache 的具体实现形式(包含 BucketCache 的内存组织模式、缓存写入读取流程等)以及配置应用形式。

1. BucketCache 的内存组织模式

下图所示为 BucketCache 的内存组织模式,图中上半局部是逻辑组织构造,下半局部是对应的物理组织构造。HBase 启动之后会在内存中申请大量的 Bucket,每个 Bucket 的大小默认为 2MB。每个 Bucket 会有一个 baseoffset 变量和一个 size 标签,其中 baseoffset 变量示意这个 Bucket 在理论物理空间中的起始地址,因而 Block 的物理地址就能够通过 baseoffset 和该 Block 在 Bucket 的偏移量惟一确定;size 标签示意这个 Bucket 能够寄存的 Block 大小,比方图中左侧 Bucket 的 size 标签为 65KB,示意能够寄存 64KB 的 Block,右侧 Bucket 的 size 标签为 129KB,示意能够寄存 128KB 的 Block。

HBase 中应用 BucketAllocator 类实现对 Bucket 的组织治理。

1)HBase 会依据每个 Bucket 的 size 标签对 Bucket 进行分类,雷同 size 标签的 Bucket 由同一个 BucketSizeInfo 治理,如上图所示,左侧寄存 64KB Block 的 Bucket 由 65KB BucketSizeInfo 治理,右侧寄存 128KB Block 的 Bucket 由 129KBBucketSizeInfo 治理。可见,BucketSize 大小总会比 Block 自身大 1KB,这是因为 Block 自身并不是严格固定大小的,总会大那么一点,比方 64K 的 Block 总是会比 64K 大一些。

2)HBase 在启动的时候就决定了 size 标签的分类,默认标签有(4+1)K,(8+1)K,(16+1)K…(48+1)K,(56+1)K,(64+1)K,(96+1)K…(512+1)K。而且零碎会首先从小到大遍历一次所有 size 标签,为每种 size 标签调配一个 Bucket,最初所有残余的 Bucket 都调配最大的 size 标签,默认调配 (512+1)K,如下图所示。

3)Bucket 的 size 标签能够动静调整,比方 64K 的 Block 数目比拟多,65K 的 Bucket 用完了当前,其余 size 标签的齐全闲暇的 Bucket 能够转换成为 65K 的 Bucket,然而会至多保留一个该 size 的 Bucket。

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

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


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

•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 数据。

文章基于《HBase 原理与实际》一书

退出移动版