1.核心组件
总入口是CacheConfig,这个类根据配置信息,来返回不同的具体cache组件;
默认会返回LruBlockCache,所有类型的block都会存入;
启用了BucketCache时,会返回CombinedBlockCache,此类中根据block类型,data block存入BucketCache,其它存入LruBlockCache;
2.LruBlockCache
LruBlockCache内部较为简单,主要就是一个map,如上图所示,由hfilename+offset来唯一标识一个block;
LruBlockCache所能够使用的内存为堆的一定比例,通过hfile.block.cache.size设置,默认是0.4;
so,maxSize = heapSize * hfile.block.cache.size,以下参数都根据maxSize计算;
acceptSize:
使用量达到一定比例时会触发驱逐,该阈值通过hbase.lru.blockcache.acceptable.factor设置,默认是0.99;
minSize:
驱逐后最少剩余比例,该阈值通过hbase.lru.blockcache.min.factor设置,默认是0.95;
hardLimit:
使用量达到一定比例时则拒绝写入,该阈值通过hbase.lru.blockcache.hard.capacity.limit.factor设置,默认是1.2,这意味允许一定的超出;
关于驱逐:
- block分为3种类型,由BlockPriority字段区分,取值为single、mutli、inMem,空间分配默认为0.25:0.5:0.25;
- 系统表以及其它指定了InMem的表所含block会标记为inMem,其它block初次存入时标记为single,再次访问时会修改为multi;
- 存放时只要还有空间即可放入,空间分配比例只是在驱逐发生时进行计算使用;
- 驱逐时,会用minSize乘以各类型的比例,得到各类型最少要保留的minSize;
- 根据目前的算法,驱逐后的size,应该是略大于minSize的一个值,伪代码如下;
expectFreeSize = usedSize - minSize;//预期释放总大小
freedSize = 0;//当前已释放总大小
n=3;//类型数量
for type in ('single','multi','inMem'):
overFlow = type.usedSize - type.minSize
toBeFree = min(overFlow,(expectFreeSize - freedSize)/n)
free(toBeFree)
freedSize += toBeFree
n--;
3.BucketCache
LruBlockCache的优点是实现简单,缺点是block的存入和释放伴随着内存的申请和释放,会带来内存碎片和gc过多的问题;
BucketCache采用了类似池的思路,预先申请内存并划分为一个个的bucket,这些bucket会一直存在并重复使用;
总体的读写流程如下图所示:
Block缓存写入流程:
- 将block写入RAMCache,然后系统会根据blockkey进行hash,根据hash结果将block分配到一组blockingQueue中;
- HBase会同时启动多个WriteThead,分别关联一个blockingQueue,并发的执行异步写入;
- 每个WriteThead读取到block数据后,调用bucketAllocator为这些block分配内存空间;
- BucketAllocator会选择与block大小对应的bucket进行存放,并且返回对应的物理地址偏移量offset;
- WriteThead将block以及分配好的物理地址偏移量传给IOEngine模块,执行具体的内存写入操作;
- 写入成功后,将类似这样的映射关系写入BackingMap中,方便后续查找时根据blockkey可以直接定位;
Block缓存读取流程:
- 首先从RAMCache中查找,对于还没有来得及写入到bucket的缓存block,一定存储在RAMCache中;
- 如果在RAMCache中没有找到,再在BackingMap中根据blockKey找到对应entry;
- 根据entry中的offset可以直接从内存中查找对应的block数据;
其中最核心的组件是BucketAllocator和IoEngine,前者负责block的逻辑地址分配,后者负责block的实际物理存放,内部结构如下:
hbase中blocksize是可以灵活设置的,bucketCache预设了一组支持的大小,从4K~512k不等;
一个Bucket只能存放一种size的block,一种size对应一个BucketSizeInfo进行管理;
初始化时,每种size先分配1个bucket,剩余的都分配给最大的那个size,如黑色箭头所示;
分配过程中当前size如果空间不够,会挪用其它size的空闲bucket,如棕色箭头所示,这意味着有可能某个Bucket一开始存放了32k的block
,后面释放后空闲,被挪用后变成存放64k的block;
ioEngine有多种实现,可支持onheap、offheap、disk等;
关于驱逐:
- 2种情况下会触发,1是已使用超过95%(acceptableFactor),2是某个size的block分配不了(总量虽然没达到阈值,但不存在完全空闲的bucket供挪用);
- 驱逐后的最少剩余比例为85%(minFactor),遍历各个bucketSizeInfo,把超过85%的部分加起来,再乘以一个系数0.1(extraFreeFactor),就是要释放的大小;
- 具体计算方法复用了LruBlockCache的代码,也是按照single、multi、inMem及其比例进行计算和释放;
- 实际清理动作是修改一些状态数据,比如Bucket对象的freeList、freeCount,以及backMapping的键值对等,并不需要对底层的byteBuffer做什么操作;
- 对于refCount大于0的block,会先将其markedForEvict置为true,待各个使用方读取完成后调用returnBlock进行释放;
参考资料
http://hbasefly.com/2016/04/26/hbase-blockcache-2/?xuxezc=17idz1