当前位置: 首页>后端>正文

HBase——BlockCache(块缓存)缓存机制

前言

众所周知,提升数据库读取性能的一个核心方法是,尽可能将热点数据存储到内存中,以避免昂贵的IO开销。现代系统架构中,诸如Redis这类缓存组件已经是体系中的核心组件,通常将其部署在数据库的上层,拦截系统的大部分请求,保证数据库的“安全”,提升整个系统的读取效率。

同样为了提升读取性能,HBase也实现了一种读缓存结构——BlockCache。客户端读取某个Block,首先会检查该Block是否存在于Block Cache,如果存在就直接加载出来,如果不存在则去HFile文件中加载,加载出来之后放到Block Cache中,后续同一请求或者邻近数据查找请求可以直接从内存中获取,以避免昂贵的IO操作。

HBase在实现中提供了两种缓存结构:MemStore和BlockCache。MemStore 作为 HBase 的写缓存,保存着数据的最近一次更新,响应的 BlockCache 作为 HBase 的读缓存,保存着最近被访问的数据块。

HBase中Block的概念

  • 1、Block是HBase中最小的数据存储单元,默认为64K,在建表语句中可以通过参数BlockSize指定。
  • 2、HBase中Block分为四种类型:Data Block,Index Block,Bloom Block和Meta Block。
  • 3、其中Data Block用于存储实际数据,通常情况下每个Data Block可以存放多条KeyValue数据对;
  • 4、Index Block和Bloom Block都用于优化随机读的查找路径,
  • 5、其中Index Block通过存储索引数据加快数据查找,
  • 6、而Bloom Block通过一定算法可以过滤掉部分一定不存在待查KeyValue的数据文件,减少不必要的IO操作;
  • 7、Meta Block主要存储整个HFile的元数据。

MemStore

  • 1、其中MemStore称为写缓存
  • 2、HBase执行写操作首先会将数据写入MemStore,并顺序写入HLog,

//代码中这样,我们的理解为 先顺序写入HLog 再将数据写入MemStore

  • 3、等满足一定条件后统一将MemStore中数据刷新到磁盘,这种设计可以极大地提升HBase的写性能。
  • 4、MemStore对于读性能也至关重要,假如没有MemStore,读取刚写入的数据就需要从文件中通过IO查找,这种代价显然是昂贵的!

一、BlockCache

从字面意思可以看出来,BlockCache主要用来缓存Block。需要关注的是,Block是HBase中最小的数据读取单元,即数据从HFile中读取都是以Block为最小单元执行的。

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

HBase 提供了几种 BlockCache 方案:

  • LRUBlockCache
  • SlabCache,HBASE-4027 0.92版本提供,在1.0版本后被废弃 HBASE-11307
  • BucketCache,HBASE-7404 0.95版本提供
  • ExternalBlockCache,HBASE-13170 1.10版本提供

这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会被置换出来。在具体的实现细节方面,需要关注以下三点。

2.1 缓存分层策略

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

  • single-access 优先级:当一个数据块第一次从HDFS读取时,它会具有这种优先级,并且在缓存空间需要被回收(置换)时,它属于优先被考虑范围内。它的优点在于:一般被扫描(scanned)读取的数据块,相较于之后会被用到的数据块,更应该被优先清除。

  • multi-access优先级:如果一个数据块,属于Single Access优先级,但是之后被再次访问,则它会升级为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等元数据挤出内存,严重影响所有业务性能。

缓存分层的好处在于:

  • 首先,通过 in memory 类型缓存,将重要的数据放到 RegionServer 内存中常驻,例如 Meta 或者 namespace 的元数据信息。
  • 其次,通过区分 single 和 multi 类型缓存,可以防止由于 scan 操作带来的 cache 频繁更替。
  • 默认配置下,对于整个 BlockCache,按照以下百分比分配给 single、multi 和 in memory 使用:0.25、0.5和0.25。无论哪个区,都会采用严格的 Least-Recently-Used 算法淘汰机制,最少使用的 Block 会被替换,为新加载的 Block 预留空间。

如果只使用 LruBlockCache,在内存较大时会存在GC的问题导致服务中断。

2.2 LRU淘汰算法实现

在每次cache block时,系统将BlockKey和Block放入HashMap后都会检查BlockCache总量是否达到阈值,如果达到阈值,就会唤醒淘汰线程对Map中的Block进行淘汰。系统设置3个MinMaxPriorityQueue,分别对应上述3个分层,每个队列中的元素按照最近最少被使用的规则排列,系统会优先取出最近最少使用的Block,将其对应的内存释放。可见,3个分层中的Block会分别执行LRU淘汰算法进行淘汰。

2.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和file。

  • heap模式表示这些Bucket是从JVM Heap中申请的;
  • offheap模式使用DirectByteBuffer技术实现堆外内存存储管理;
  • file模式使用类似SSD的存储介质来缓存Data Block。

其次支持了多种不同大小的 bucket,以适应不同大小的 block size。可以通过参数 hbase.bucketcache.bucket.sizes 来配置不同 bucket 的大小。默认是14种,大小分别是4、8、16、32、40、48、56、64、96、128、192、256、384、512KB的block(逗号分隔)。并且,在某一大小类型的 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的内存组织形式、缓存写入读取流程等)以及配置使用方式。

五、ExternalBlockCache

ExternalBlockCache 提供使用外部的缓存服务来进行缓存,如 memcached 和 redis 等。

更具体的缓存细节参考 HBase BlockCache源码

六、HBase 读路径

总结,HBase 读路径为,首先检查 MemStore,然后检查 BlockCache,最后检索 HFile,并且合并一条数据的信息(read merge)返回给客户端。

七、BucketCache详解

7.1 BucketCache的内存组织形式

HBase——BlockCache(块缓存)缓存机制,第1张

上图所示为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,如下图所示。


    HBase——BlockCache(块缓存)缓存机制,第2张

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

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


HBase——BlockCache(块缓存)缓存机制,第3张

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

参考:
https://www.shuzhiduo.com/A/x9J2LNrKd6/

https://blog.csdn.net/Shockang/article/details/125951824

https://segmentfault.com/a/1190000023408296

https://blog.csdn.net/weixin_42073408/article/details/119637260

https://www.cnblogs.com/zackstang/p/10061379.html


https://www.xamrdz.com/backend/3ns1927043.html

相关文章: