HBase的Block cache

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时几乎不发生

 

 

 

你可能感兴趣的:(cache)