HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分做BlockCache,用来读,当然Memstore也有读的功效,不过由于Hbase的scan机制,从Memsotre读到数据的效果一般。
今天主要来分析下Hbase的BlockCache机制,并且阐述其中碰到的一个RTE异常。
话不多说,首先来看看Hbase的存储机制。
其实际存储文件是HFile格式的,这是一个HDFS上的Seq的二进制流
HFILE由以下几部分组成
DATABLOCK DATABLOCK 。。。标志 METABLOCK METABLOCK 。。。DATAINDEX METAINDEX TRAILER
TRAILER为头指针,指向DATAINDEX和METAINDEX,DATAINDEX和METAINDEX分别指向BLOCK的起始位置
DATABLOCK的结构是keyvalue键值对,具体的结构就不分析了
LRU顾名思义就是最近最久未使用方法,查找到内存中最近最久未使用的进行淘汰
RS在启动的时候会启动一个线程专门进行LRU淘汰如下所示:
public void run() { while(true) { synchronized(this) { try { this.wait(); } catch(InterruptedException e) {} } LruBlockCache cache = this.cache.get(); if(cache == null) break; cache.evict(); } }
该段代码是一个循环等待LRU进程唤醒条件的发生,当该条件满足时会调用cache的evict()方法进行淘汰。
那么产生的条件是什么呢
整个RS的LRU维护一个HashMap<string,Block>,String 是一个blockpath,由storefilepath+标记+blocknum组成
当有读请求发生时首先会对当前storefile的read对象,synchronized (metaIndex.blockKeys[block])
然后get,若无则加进map中。有则直接返回Block
当加入map以后map的size达到一个阈值,此时:
public void evict() { synchronized(this) { this.notify(); // FindBugs NN_NAKED_NOTIFY } }
该函数会通知LRU线程进行淘汰。淘汰的规则如下:
将MAP中的数据装入3个桶:In-Memory,muti,single 。3个桶都有容量,且各自维护一个list用来存放放入其中的Block
删除的是批量删除的形式,即如果Map的size大于阈值A,那么此时会释放到一个MAP的初始值大小。
首先选择将要删除的桶,超过桶容量最大的优先选择删除算法如下所示
while((bucket = bucketQueue.poll()) != null) { long overflow = bucket.overflow(); if(overflow > 0) { long bucketBytesToFree = Math.min(overflow, (bytesToFree - bytesFreed) / remainingBuckets); bytesFreed += bucket.free(bucketBytesToFree); } remainingBuckets--; }
cache可以高效地提升hbase的读TPS,在hbase中具有重要的作用。
Memstore则是写操作的关键,二者在hbase都有重要的作用,当然是越大越好。不过hbase规定二者之和不得超过可用内存的0.8呗,否则会产生不可预知的错误。
下面来看看碰到的一个RTE异常
synchronized (metaIndex.blockKeys[block]) { metaLoads++; // Check cache for block. If found return. if (cache != null) { ByteBuffer cachedBuf = cache.getBlock(name + "meta" + block, cacheBlock); if (cachedBuf != null) { // Return a distinct 'shallow copy' of the block, // so pos doesnt get messed by the scanner cacheHits++; return cachedBuf.duplicate(); } // Cache Miss, please load. } ByteBuffer buf = decompress(metaIndex.blockOffsets[block], longToInt(blockSize), metaIndex.blockDataSizes[block], true); byte [] magic = new byte[METABLOCKMAGIC.length]; buf.get(magic, 0, magic.length); if (! Arrays.equals(magic, METABLOCKMAGIC)) { throw new IOException("Meta magic is bad in block " + block); } // Create a new ByteBuffer 'shallow copy' to hide the magic header buf = buf.slice(); readTime += System.currentTimeMillis() - now; readOps++; readData +=longToInt(blockSize); // Cache the block if(cacheBlock && cache != null) { cache.cacheBlock(name + "meta" + block, buf.duplicate(), inMemory); } return buf; }
这段代码逻辑性本身是没有问题,但是当出现以下情况时就有触发异常
LRU维护一个HashMap<string,Block>,String 是一个blockpath,由storefilepath+标记+blocknum组成。
当有读请求发生时首先会对当前storefile的read对象,synchronized (metaIndex.blockKeys[block])
然后get,若无则加进map中。有则直接返回Block
但是这个逻辑在Region split和compact的时候会有问题。
假设A split B 和C,此时读取C,那么首先会去get,发现不存在,然后将C的blockpath putin map。
如果compact刚好在get和put之间结束,此时又有读请求过来,那么此时它们的storefilepath是一样的;
由于是getMetaBlock,因此它们的blocknum也一致,故blockpath也一致;
而且compact结束以后创建的是新的reader对象,与C原来的HalfStoreFileReader对象不一致。此时该读请求也会去get map,发现map中没有该blockpath,也会执行put操作。
这样对同一block会有两次put,会抛出RTE: Cached an already cached block 目前来看这个异常不会造成数据丢失,对读操作也无影响,主要发生在getMetaBlock的时候。
由于compact以后对于数据所在block的number进行了更新故在LoadBlock时几乎不发生
目前来看这个异常不会造成数据丢失,对读操作也无影响,主要发生在getMetaBlock的时候。
由于compact以后对于数据所在block的number进行了更新故在LoadBlock时几乎不发生