背景:
1、缓存对于数据库来说极其的重要
2、最理想的情况是,所有数据都能够缓存到内存,这样就不会有任何文件IO请求,读写性能必然会提升到极致。
3、我们并不需要将所有数据都缓存起来,根据二八法则,80%的业务请求都集中在20%的热点数据上,
4、把20%的数据缓存起来,将这部分数据缓存起就可以极大地提升系统性能。

HBase在实现中提供了两种缓存结构:MemStore和BlockCache。
MemStore
1、其中MemStore称为写缓存
2、HBase执行写操作首先会将数据写入MemStore,并顺序写入HLog,
//代码中这样,我们的理解为 先顺序写入HLog 再将数据写入MemStore
3、等满足一定条件后统一将MemStore中数据刷新到磁盘,这种设计可以极大地提升HBase的写性能。
4、MemStore对于读性能也至关重要,假如没有MemStore,读取刚写入的数据就需要从文件中通过IO查找,这种代价显然是昂贵的!

BlockCache
1、BlockCache称为读缓存
2、HBase会将一次文件查找的Block块缓存到Cache中,以便后续同一请求或者邻近数据查找请求,可以直接从内存中获取,避免昂贵的IO操作。

简单地回顾一下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的元数据。

1、BlockCache是Region Server级别的,
2、一个Region Server只有一个Block Cache,在Region Server启动的时候完成Block Cache的初始化工作。
3、到目前为止,HBase先后实现了3种Block Cache方案,LRUBlockCache是最初的实现方案,也是默认的实现方案;HBase 0.92版本实现了第二种方案SlabCache,见HBASE-4027;HBase 0.96之后官方提供了另一种可选方案BucketCache,见HBASE-7404。
4、这三种方案的不同之处在于对内存的管理模式,
5、其中LRUBlockCache是将所有数据都放入JVM Heap中,交给JVM进行管理。
6、SlabCache BucketCache 这两种采用了不同机制将部分数据存储在堆外,交给HBase自己管理。
7、这种演变过程是因为LRUBlockCache方案中JVM垃圾回收机制经常会导致程序长时间暂停,而采用堆外内存对数据进行管理可以有效避免这种情况发生。


LRUBlockCache //HBase默认的BlockCache实现方案
1、将内存从逻辑上分为了三块:single-access区、mutil-access区、in-memory区,分别占到整个BlockCache大小的25%、50%、25%
2、一次随机读中,一个Block块从HDFS中加载出来之后首先放入signle区,
3、后续如果有多次请求访问到这块数据的话,就会将这块数据移到mutil-access区。
3、而in-memory区表示数据可以常驻内存,一般用来存放访问频繁、数据量小的数据,比如元数据,用户也可以在建表的时候通过设置列族属性IN-MEMORY= true将此列族放入in-memory区。 //这一部分参考 HBase - 建表语句解析 http://hbasefly.com/2016/03/23/hbase_create_table/ 中提到的 IN_MEMORY 参数
4、很显然,这种设计策略类似于JVM中young区、old区以及perm区。

SlabCache
1、为了解决LRUBlockCache方案中因为JVM垃圾回收导致的服务中断,SlabCache方案使用Java NIO DirectByteBuffer技术实现了堆外内存存储,不再由JVM管理数据内存。
2、默认情况下,系统在初始化的时候会分配两个缓存区,分别占整个BlockCache大小的80%和20%,每个缓存区分别存储固定大小的Block块,
//具体见博客内容,
缺点为:
线上集群环境中,不同表不同列族设置的BlockSize都可能不同,很显然,默认只能存储两种固定大小Block的SlabCache方案不能满足部分用户场景,
因此HBase实际实现中将SlabCache和LRUBlockCache搭配使用,称为DoubleBlockCache。
1、DoubleBlockCache方案有很多弊端。比如SlabCache设计中固定大小内存设置会导致实际内存使用率比较低,
2、而且使用LRUBlockCache缓存Block依然会因为JVM GC产生大量内存碎片。
3、因此在HBase 0.98版本之后,该方案已经被不建议使用。

BucketCache //阿里设计出来的 cdh用的这种
1、SlabCache方案在实际应用中并没有很大程度改善原有LRUBlockCache方案的GC弊端,还额外引入了诸如堆外内存使用率低的缺陷。然而它的设计并不是一无是处,至少在使用堆外内存这个方面给予了阿里大牛们很多启发。站在SlabCache的肩膀上,他们开发了BucketCache缓存方案并贡献给了社区。
2、实际实现中,HBase将BucketCache和LRUBlockCache搭配使用,称为CombinedBlockCache。
3、和DoubleBlockCache不同,系统在LRUBlockCache中主要存储Index Block和Bloom Block,而将Data Block存储在BucketCache中
4、因此一次随机读需要首先在LRUBlockCache中查到对应的Index Block,然后再到BucketCache查找对应数据块。BucketCache通过更加合理的设计修正了SlabCache的弊端,极大降低了JVM GC对业务请求的实际影响,
5、但也存在一些问题,比如使用堆外内存会存在拷贝内存的问题,一定程度上会影响读写性能。当然,在后来的版本中这个问题也得到了解决,

LRUBlockCache //升入了解
1、它使用一个ConcurrentHashMap管理BlockKey到Block的映射关系,
2、缓存Block只需要将BlockKey和对应的Block放入该HashMap中,查询缓存就根据BlockKey从HashMap中获取即可。
3、同时该方案采用严格的LRU淘汰算法,当Block Cache总量达到一定阈值之后就会启动淘汰机制,最近最少使用的Block会被置换出来。在具体的实现细节方面,需要关注三点:

  1. 缓存分层策略
    //将整个BlockCache分为三个部分:single-access、mutil-access和inMemory。需要特别注意的是,HBase系统元数据存放在InMemory区,因此设置数据属性InMemory = true需要非常谨慎,确保此列族数据量很小且访问频繁,否则有可能会将hbase.meta元数据挤出内存,严重影响所有业务性能。

  2. LRU淘汰算法实现

  3. LRU方案优缺点
    //LRU方案使用JVM提供的HashMap管理缓存,简单有效。
    但是:会出现full gc 碎片空间一直累计就会产生臭名昭著的Full GC。
    尤其在大内存条件下,一次Full GC很可能会持续较长时间,甚至达到分钟级别。大家知道Full GC是会将整个进程暂停的(称为stop-the-wold暂停),因此长时间Full GC必然会极大影响业务的正常读写请求。

BucketCache //升入了解概念 见博客
//它没有使用JVM 内存管理算法来管理缓存,而是自己对内存进行管理,因此不会因为出现大量碎片导致Full GC的情况发生。

内存组织形式

Block缓存写入、读取流程

BucketCache工作模式

BucketCache配置使用 //重点 //cdh用的这种模式
//
BucketCache 的总大小,以 MB 为单位。要配置的大小取决于可供 HBase 使用的内存量或本地 SSD 的大小。如果将 hbase.bucketcache.ioengine 设为“offheap”,则 BucketCache 会消耗 Java 的直接内存中的已配置内存量。
hbase.bucketcache.size = 1M

提示:
其中heap模式和offheap模式都使用内存作为最终存储介质,内存分配查询也都使用Java NIO ByteBuffer技术,不同的是,heap模式分配内存会调用byteBuffer.allocate方法,从JVM提供的heap区分配,而后者会调用byteBuffer.allocateDirect方法,直接从操作系统分配。
这两种内存分配模式会对HBase实际工作性能产生一定的影响。影响最大的无疑是GC ,相比heap模式,offheap模式因为内存属于操作系统,所以基本不会产生CMS GC,也就在任何情况下都不会因为内存碎片导致触发Full GC。
除此之外,在内存分配以及读取方面,两者性能也有不同,比如,内存分配时heap模式需要首先从操作系统分配内存再拷贝到JVM heap,相比offheap直接从操作系统分配内存更耗时;但是反过来,读取缓存时heap模式可以从JVM heap中直接读取,而offheap模式则需要首先从操作系统拷贝到JVM heap再读取,显得后者更费时。

file模式和前面两者不同,它使用Fussion-IO或者SSD等作为存储介质,相比昂贵的内存,这样可以提供更大的存储容量,因此可以极大地提升缓存命中率。

参考链接:
HBase BlockCache系列 – 走进BlockCache http://hbasefly.com/2016/04/08/hbase-blockcache-1/
HBase BlockCache系列 - 探求BlockCache实现机制 http://hbasefly.com/2016/04/26/hbase-blockcache-2/