[转]HBase BlockCache机制讲解&源码分析

来源:互联网 发布:价格水平 知乎 编辑:程序博客网 时间:2024/05/18 20:06

原文链接:http://blog.csdn.net/bryce123phy/article/details/62051927


Hbase上RegionServer的cache主要分为两个部分,分别是memstore&blockcache,其中memstore主要用于写缓存,而blockcache用于读缓存。

当数据写入hbase时,会先写入memstore,RegionServer会给每个region提供一个memstore,memstore中的数据达到系统设置的水位值后,会触发flush将memstore中的数据刷写到磁盘。

客户的读请求会先到memstore中查数据,若查不到就到blockcache中查,再查不到就会从磁盘上读,并把读入的数据同时放入blockcahce。我们知道缓存有三种不同的更新策略,分别是先入先出(FIFO)、LRU(最近最少使用)和LFU(最近最不常使用),hbase的block使用的是LRU策略,当BlockCache的大小达到上限后,会触发缓存淘汰机制,将最老的一批数据淘汰掉。

一个RegionServer上有一个BlockCache和N个Memstore。下面我们从hbase的源码中展开阐述Blockcache的具体实现,并在讲解实现的中间补充阐述关于缓存的相关机制介绍。

BlockCache在HBase中所处的位置如下图中所示:


BlockCache的实现是基于On-heap ConcurrentHashMap。map的key是BlockCacheKey类型的对象,包括了offset、hfileName等成员变量,map的value是LruCachedBlock类型的对象,表示缓存的实体,该对象中定义了成员变量accesstime,用于LRU淘汰时的比较依据。BlockCache的大小是固定的,由参数hfile.block.cache.size决定,默认是RegionServer的堆内存的40%。

BlockCache的初始化在HRegionServer的handleReportForDutyResponse里完成,HRegionServer有一个HeapMemoryManager类型的成员变量,用于管理RegionServer进程的堆内存,HeapMemoryManager中的blockCache就是RegionServer中的读缓存,它的初始化在CacheConfig的instantiateBlockCache方法中完成,剪掉一些判断BlockCache是否禁用的代码,我们列出其中的主要逻辑如下:

[java] view plain copy
  1. public static synchronized BlockCache instantiateBlockCache(Configuration conf) {  
  2.   MemoryUsage mu = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();  
  3.   LruBlockCache l1 = getL1(conf, mu);  
  4.   BlockCache l2 = getL2(conf, mu);  
  5.   if (l2 == null) {  
  6.     GLOBAL_BLOCK_CACHE_INSTANCE = l1;  
  7.   } else {  
  8.     boolean useExternal = conf.getBoolean(EXTERNAL_BLOCKCACHE_KEY, EXTERNAL_BLOCKCACHE_DEFAULT);  
  9.     boolean combinedWithLru = conf.getBoolean(BUCKET_CACHE_COMBINED_KEY,  
  10.       DEFAULT_BUCKET_CACHE_COMBINED);  
  11.     if (useExternal) {  
  12.       GLOBAL_BLOCK_CACHE_INSTANCE = new InclusiveCombinedBlockCache(l1, l2);  
  13.     } else {  
  14.       if (combinedWithLru) {  
  15.         GLOBAL_BLOCK_CACHE_INSTANCE = new CombinedBlockCache(l1, l2);  
  16.       } else {  
  17.         GLOBAL_BLOCK_CACHE_INSTANCE = l1;  
  18.       }  
  19.     }  
  20.     l1.setVictimCache(l2);  
  21.   }  
  22.   return GLOBAL_BLOCK_CACHE_INSTANCE;  
  23. }  

其中的GLOBAL_BLOCK_CACHE_INSTANCE是CacheConfig中维护的静态BlockCache实例,也就是我们要返回给RegionServer的读缓存。注意代码中对useExternal和combinedWithLru的判断,如果指定了useExternal为true,则结合memcached等外部缓存与BlockCache一起使用。如果指定了combinedWithLru,则结合bucketCache,也就是堆外内存与BlockCache一起使用。在上述两种情况下,BlockCache用于存放索引等元数据,真实的数据文件则缓存在memcached或bucketCache中。

如果想使用上述两种特性,可以分别将"hbase.block.use.external"或"hbase.bucketcache.combinedcache.enabled"设置为true。其中,外部缓存对象经hbase.blockcache.external.class由反射方法注入,关于hbase中外部缓存的使用可以参看HBase的issue13170,里面有详细的介绍。

BlockCache基于客户端对数据的访问频率,定义了三个不同的优先级,如下所示:

SINGLE:如果一个Block被第一次访问,则该Block被放在这一优先级队列中;

MULTI:如果一个Block被多次访问,则从single移到Multi中;

MEMORY:memory优先级由用户指定,一般不推荐,只用系统表才使用memory优先级;

以上将cache分级的好处在于:

首先,通过Memory类型的cache,可以将重要的数据放到RegionServer内存中常驻,例如Meta或者namespace的元数据信息;

其次,通过区分single和multi类型cache,可以防止由于scan操作带来的cache频繁颠簸,将最少使用的block加入到淘汰算法中;

默认配置下,对于整个blockcache的内存,按照以下百分比分配给single、multi和inMemory使用:0.25、0.5和0.25;

下面我们分析将Block块加入缓存的实现,主要代码如下所示:

[java] view plain copy
  1. public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory,  
  2.     final boolean cacheDataInL1) {  
  3.   //首先判断cacheKey是否已被缓存,省略  
  4.   
  5.   LruCachedBlock cb = new LruCachedBlock(cacheKey, buf, count.incrementAndGet(), inMemory);  //创建BlockCache中的实体  
  6.   long newSize = updateSizeMetrics(cb, false);  //更新metrics  
  7.   map.put(cacheKey, cb);  
  8.     
  9.   if (newSize > acceptableSize() && !evictionInProgress) { //如果cache达到大小限制,执行evict逻辑  
  10.     runEviction();  
  11.   }  
  12. }  
1、首先假设不会对同一个已经被缓存的BlockCacheKey重复放入cache操作;

2、根据是否inmemory创建不同类别的CachedBlock对象:若inMemory为true则创建BlockPriority.MEMORY类型,否则创建BlockPriority.SINGLE类型;

3、将BlockCacheKey和创建的CachedBlock对象加入到前文说过的ConcurrentHashMap中,同时更新log&metrics上的计数;

4、最后判断如果加入新block后cache size大于设定的临界值且当前没有淘汰线程运行,则调用runEviction()方法启动LRU淘汰线程,runEviciton方法如下:

[java] view plain copy
  1. private void runEviction() {  
  2.   if (evictionThread == null) {  
  3.      evict();   
  4.   } else {  
  5.      evictionThread.evict();  
  6.   }  
  7. }  
其中淘汰线程evictionThread在LruBlockCache初始化的同时创建,并且指定为守护daemon线程;

evictionThread用于与主线程同步完成block cache的淘汰过程,该过程的主要逻辑在run方法中:

[java] view plain copy
  1. public void run() {  
  2.   enteringRun = true;  
  3.   while (this.go) {  
  4.     synchronized(this) {  
  5.       try {  
  6.         this.wait(1000 * 10/*Don't wait for ever*/);  
  7.       } catch(InterruptedException e) {  
  8.         LOG.warn("Interrupted eviction thread ", e);  
  9.         Thread.currentThread().interrupt();  
  10.       }  
  11.     }  
  12.     LruBlockCache cache = this.cache.get();  
  13.     if (cache == nullbreak;  
  14.     cache.evict();  
  15.   }  
  16. }  

evictionThread线程启动后,调用wait被阻塞住,直到EvictionThread线程的evict方法被runEviction调用后,evict中执行notifyAll唤醒被阻塞住的evictionThread主线程,主线程继续执行LruBlockCache的evict方法进行真正的淘汰过程。evict方法的主流程如下所示:

[java] view plain copy
  1. void evict() {  
  2.   if(!evictionLock.tryLock()) return;  
  3.   
  4.   try {  
  5.     evictionInProgress = true;  
  6.     long currentSize = this.size.get();  
  7.     long bytesToFree = currentSize - minSize();  
  8.      
  9.     if(bytesToFree <= 0return;  
  10.   
  11.     BlockBucket bucketSingle = new BlockBucket("single", bytesToFree, blockSize, singleSize());  
  12.     BlockBucket bucketMulti = new BlockBucket("multi", bytesToFree, blockSize, multiSize());  
  13.     BlockBucket bucketMemory = new BlockBucket("memory", bytesToFree, blockSize, memorySize());  
  14.   
  15.     for(LruCachedBlock cachedBlock : map.values()) {  
  16.       switch(cachedBlock.getPriority()) {  
  17.         case SINGLE: {  
  18.           bucketSingle.add(cachedBlock);  
  19.           break;  
  20.         }  
  21.         case MULTI: {  
  22.           bucketMulti.add(cachedBlock);  
  23.           break;  
  24.         }  
  25.         case MEMORY: {  
  26.           bucketMemory.add(cachedBlock);  
  27.           break;  
  28.         }  
  29.       }  
  30.     }  
  31.   
  32.     long bytesFreed = 0;  
  33.     if (forceInMemory || memoryFactor > 0.999f) {  
  34.       long s = bucketSingle.totalSize();  
  35.       long m = bucketMulti.totalSize();  
  36.       if (bytesToFree > (s + m)) {  
  37.         bytesFreed = bucketSingle.free(s);  
  38.         bytesFreed += bucketMulti.free(m);  
  39.           
  40.         bytesFreed += bucketMemory.free(bytesToFree - bytesFreed);  
  41.       } else {  
  42.         long bytesRemain = s + m - bytesToFree;  
  43.         if (3 * s <= bytesRemain) {  
  44.           bytesFreed = bucketMulti.free(bytesToFree);  
  45.         } else if (3 * m <= 2 * bytesRemain) {  
  46.           bytesFreed = bucketSingle.free(bytesToFree);  
  47.         } else {  
  48.           bytesFreed = bucketSingle.free(s - bytesRemain / 3);  
  49.           if (bytesFreed < bytesToFree) {  
  50.             bytesFreed += bucketMulti.free(bytesToFree - bytesFreed);  
  51.           }  
  52.         }  
  53.       }  
  54.     } else {  
  55.       PriorityQueue<BlockBucket> bucketQueue =  
  56.         new PriorityQueue<BlockBucket>(3);  
  57.   
  58.       bucketQueue.add(bucketSingle);  
  59.       bucketQueue.add(bucketMulti);  
  60.       bucketQueue.add(bucketMemory);  
  61.   
  62.       int remainingBuckets = 3;  
  63.   
  64.       BlockBucket bucket;  
  65.       while((bucket = bucketQueue.poll()) != null) {  
  66.         long overflow = bucket.overflow();  
  67.         if(overflow > 0) {  
  68.           long bucketBytesToFree = Math.min(overflow,  
  69.               (bytesToFree - bytesFreed) / remainingBuckets);  
  70.           bytesFreed += bucket.free(bucketBytesToFree);  
  71.         }  
  72.         remainingBuckets--;  
  73.       }  
  74.     }  
  75.   } finally {  
  76.     stats.evict();  
  77.     evictionInProgress = false;  
  78.     evictionLock.unlock();  
  79.   }  
  80. }  
下面我们跟着代码详解evict中每一步的实现及含义:

1、首先获取锁,保证同一时刻只有一个淘汰线程正在运行;

2、计算得到当前block cache总大小currentSize以及需要被淘汰释放掉的大小bytesToFree,如果bytesToFree小于等于0则不进行后续操作;

3、初始化创建三个BlockBucket对象,对象中包含了一个元素为LruCachedBlock的MinMaxPriorityQueue队列,分别用于三种优先级的cahceBlock对象,队列按LRU(最近最少使用)的原则维护BlockBucket中缓存住的所有对象;

4、遍历全局ConcurrentHashMap中的所有BlockCache,依类型加入到相应的BlockBucket队列中;

5、如果指定了放入最高优先级memory,则根据single、multi和bytesFreed三者之间的关系计算在各个队列中需要释放的空间,此种情况不推荐,因此不再细述;

6、将以上三个BlockBucket队列加入到一个优先级队列bucketQueue中,队列按照各个BlockBucket超出指定bucketSize的大小(overflow)顺序排序;

7、遍历优先级队列,对于每个BlockBucket,通过Math.min(overflow,(bytesToFree - bytesFreed)/remainingBuckets)计算出需要释放的空间大小,这样做可以保证尽可能平均地从三个BlockBucket释放LruCachedBlock。释放过程在BlockBucket的free方法;

8、具体到free方法,它每次从BlockBucket维护的队尾取出一个LruCachedBlock对象并调用evictBlock方法,evictBlock将LruCachedBlock从全局的concurrentHashMap中移除,同时更新相关计数;

9、如果有bucketCache或者memcached等其它辅助缓存,第八步总淘汰掉的CacheBlock会进入辅助缓存;

前面讲过一个LruCachedBlock如果被多次连续访问,那么它会从SINGLE优先级升级到MULTI优先级,这部分逻辑在getBlock中实现,getBlock接收用户传入的BlockCacheKey,并返回该Key制定的LruCachedBlock,如果目标LruCachedBlock在全局map中存在,那么会触发LruCachedBlock的access执行,将目标LruCachedBlock从SINGLE优先级调整为MULTI。

[java] view plain copy
  1. public void access(long accessTime) {  
  2.   this.accessTime = accessTime;  
  3.   if(this.priority == BlockPriority.SINGLE) {  
  4.     this.priority = BlockPriority.MULTI;  
  5.   }  
  6. }  
参数accessTime指定了调用access的次数,每调用access一次,accessTime就自增1,这是由于LruCachedBlock在BlockBucket中是按照升序排列的。accessTime越大,则LruCachedBlock在队列中的位置越靠后,执行淘汰时,就越晚被淘汰。

需要注意的是在BlockCache中的数据是经过decompressed(解压缩)的,用户可将如下配置修改为true,当其为true时读入blockcache中的数据不会经过解压缩,如此可以增大单位blockcache中可缓存的数据条数,但是用户读取数据数据需要解压缩。

[java] view plain copy
  1. hbase.block.data.cachecompressed  
上述配置的默认值是false。
原创粉丝点击