背景:
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区。
阶段小结:
LRUBlockCache机制:类似于jvm的young区、old区以及perm区,他分为(single-access区、mutil-access区、in-memory区,分别占到整个BlockCache大小的25%、50%、25%)在一次随机访问数据的时候从hdfs加载出来,放到single-access区,后续如果有多次请求这块的数据,就会放到 mutil-access区 而in-memory区表示数据可以常驻内存,一般用来存放访问频繁、数据量小的数据,比如元数据。
//当BlockCache总量达到一定阈值之后就会启动淘汰机制,最少使用的Block会被置换出来,为新加载的Block预留空间。
缺点:使用LRUBlockCache缓存机制会因为CMS GC策略导致内存碎片过多,从而可能引发臭名昭著的Full GC,触发可怕的’stop-the-world’暂停,严重影响上层业务
那CMS GC策略如何导致内存碎片过多?内存碎片过多如何触发Full GC?请关注博主另一篇博客。
SlabCache //已经被淘汰了
1、为了解决LRUBlockCache方案中因为JVM垃圾回收导致的服务中断,SlabCache方案使用Java NIO DirectByteBuffer技术实现了堆外内存存储,不再由JVM管理数据内存。
2、默认情况下,系统在初始化的时候会分配两个缓存区,分别占整个BlockCache大小的80%和20%,每个缓存区分别存储固定大小的Block块,
3、其中前者主要存储小于等于64K大小的Block,后者存储小于等于128K Block,如果一个Block太大就会导致两个区都无法缓存。
4、和LRUBlockCache相同,SlabCache也使用Least-Recently-Used算法对过期Block进行淘汰。
5、和LRUBlockCache不同的是,SlabCache淘汰Block的时候只需要将对应的bufferbyte标记为空闲,后续cache对其上的内存直接进行覆盖即可。
SlabCache 和 DoubleBlockCache 缺点为:
线上集群环境中,不同表不同列族设置的BlockSize都可能不同,很显然,默认只能存储两种固定大小Block的SlabCache方案不能满足部分用户场景,
因此HBase实际实现中将SlabCache和LRUBlockCache搭配使用,称为DoubleBlockCache。
1、DoubleBlockCache方案有很多弊端。比如SlabCache设计中固定大小内存设置会导致实际内存使用率比较低,
2、而且使用LRUBlockCache缓存Block依然会因为JVM GC产生大量内存碎片。
3、因此在HBase 0.98版本之后,该方案已经被不建议使用。
阶段小结:
SlabCache:实现的是堆外内存存储,不在由JVM管理数据内存。
DoubleBlockCache:因此HBase实际实现中将SlabCache和LRUBlockCache搭配使用,称DoubleBlockCache。
为什么要搭配使用呢?或者说是SlabCache的缺点
1、线上集群环境中,不同表不同列族设置的BlockSize都可能不同,很显然,默认只能存储两种固定大小Block的SlabCache方案不能满足部分用户场景,比如用户设置BlockSize = 256K,简单使用SlabCache方案就不能达到这部分Block缓存的目的。
2、一次随机读中,一个Block块从HDFS中加载出来之后会在两个Cache中分别存储一份;缓存读时首先在LRUBlockCache中查找,如果Cache Miss再在SlabCache中查找,此时如果命中再将该Block放入LRUBlockCache中。
缺点:
DoubleBlockCache方案有很多弊端。比如SlabCache设计中固定大小内存设置会导致实际内存使用率比较低,而且使用LRUBlockCache缓存Block依然会因为JVM GC产生大量内存碎片。因此在HBase 0.98版本之后,该方案已经被不建议使用。
//已经淘汰了
BucketCache //阿里设计出来的 cdh用的这种 //可以参考:hbase针对fullgc所做的优化(Memstore所作的优化 针对BlockCache所作优化):https://blog.51cto.com/12445535/2373223
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、但也存在一些问题,比如使用堆外内存会存在拷贝内存的问题,一定程度上会影响读写性能。当然,在后来的版本中这个问题也得到了解决,
优点:
而Bucket Cache缓存机制因为在初始化的时候就申请了一片固定大小的内存作为缓存,缓存淘汰不再由 JVM管理,数据Block的缓存操作只是对这片空间的访问和覆盖,因而大大减少了内存碎片的出现,降低了Full GC发生的频率。//Bucket Cache 缓存淘汰不再由 JVM管理 降低了Full GC发生的频率。
Bucket Cache 缓存 中有3中模式 heap模式和offheap模式 file模式
offheap模式因为内存属于操作系统,所以基本不会产生CMS GC,也就在任何情况下都不会因为内存碎片导致触发Full GC。
LRUBlockCache //升入了解
1、它使用一个ConcurrentHashMap管理BlockKey到Block的映射关系,
2、缓存Block只需要将BlockKey和对应的Block放入该HashMap中,查询缓存就根据BlockKey从HashMap中获取即可。
3、同时该方案采用严格的LRU淘汰算法,当Block Cache总量达到一定阈值之后就会启动淘汰机制,最近最少使用的Block会被置换出来。在具体的实现细节方面,需要关注三点:
-
缓存分层策略
//将整个BlockCache分为三个部分:single-access、mutil-access和inMemory。需要特别注意的是,HBase系统元数据存放在InMemory区,因此设置数据属性InMemory = true需要非常谨慎,确保此列族数据量很小且访问频繁,否则有可能会将hbase.meta元数据挤出内存,严重影响所有业务性能。 -
LRU淘汰算法实现
- LRU方案优缺点
//LRU方案使用JVM提供的HashMap管理缓存,简单有效。
但是:会出现full gc 碎片空间一直累计就会产生臭名昭著的Full GC。
尤其在大内存条件下,一次Full GC很可能会持续较长时间,甚至达到分钟级别。大家知道Full GC是会将整个进程暂停的(称为stop-the-wold暂停),因此长时间Full GC必然会极大影响业务的正常读写请求。
BucketCache //升入了解概念 见博客 cdh默认是这种缓存模式
//它没有使用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等作为存储介质,相比昂贵的内存,这样可以提供更大的存储容量,因此可以极大地提升缓存命中率。
提示:
LRUBlockCache
SlabCache
DoubleBlockCache:因此HBase实际实现中将SlabCache和LRUBlockCache搭配使用,称为DoubleBlockCache //淘汰了
BucketCache //推荐用的 堆外内存机制 cdh用的这个
CombinedBlockCache:HBase将BucketCache和LRUBlockCache搭配使用,称为CombinedBlockCache。
总结:
所以经过上面的分析之后,最后剩下LRU君(LRUBlockCache)和CBC君(CombinedBlockCache)。
我们具体分析:
结论
看完了所有比较重要的指标对比数据,我们可以得出以下两点:
- 在’缓存全部命中’场景下,LRU君可谓完胜CBC君。因此如果总数据量相比JVM内存容量很小的时候,选择LRU君;
- 在所有其他存在缓存未命中情况的场景下, LRU君的GC性能几乎只有CBC君的1/3,而吞吐量、读写延迟、IO、CPU等指标两者基本相当,因此建议选择CBC。
理论解释
1、之所以在’缓存全部命中’场景下LRU的各项指标完胜CBC,
2、而在’缓存大量未命中’的场景下,LRU各项指标与CBC基本相当,
3、是因为HBase在读取数据的时候,如果都缓存命中的话,对于CBC,需要将堆外内存先拷贝到JVM内,然后再返回给用户,流程比LRU君的堆内内存复杂,延迟就会更高。
4、而如果大量缓存未命中,内存操作就会占比很小,延迟瓶颈主要在于IO,使得LRU和CBC两者各项指标基本相当。
参考链接:
HBase BlockCache系列 – 走进BlockCache http://hbasefly.com/2016/04/08/hbase-blockcache-1/
HBase BlockCache系列 - 探求BlockCache实现机制 http://hbasefly.com/2016/04/26/hbase-blockcache-2/
HBase BlockCache系列 - 性能对比测试报告 http://hbasefly.com/2016/05/06/hbase-blockcache-3/
转载于:https://blog.51cto.com/12445535/2363376