家喻户晓,晋升数据库读取性能的一个外围办法是,尽可能将热点数据存储到内存中,以防止低廉的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原理与实际》一书