[HBase] - 读懂 BucketCache 日志信息
这是 HBase regionserver 打印的一段 bucket cache 统计信息,里面有些指标有点让人摸不着头脑。下面将通过提问的方式,结合实际业务与源码逐一解释各个指标,分析可以尝试优化的点。
2022-05-09 16:26:31,240 INFO [BucketCacheStatsExecutor] bucket.BucketCache: failedBlockAdditions=20681097, totalSize=60.00 GB, freeSize=6.75 GB, usedSize=53.25 GB, cacheSize=52.43 GB, accesses=41703312106, hits=37430928681, IOhitsPerSecond=1577, IOTimePerHit=0.06, hitRatio=89.76%, cachingAccesses=38812699538, cachingHits=37362791550,cachingHitsRatio=96.26%, evictions=14844, evicted=1384179287, evictedPerRun=93248.40625
问题1 : failedBlockAdditions 是什么?
写缓存失败的次数,写缓存流程大概如下,失败出现在第 2 步:
- 写入 ramCache
- 写入 write queue,它是一个阻塞队列 ArrayBlockingQueue。入队 queue.offer() 失败的时候,将失败次数记录写到:failedBlockAdditions
RAMQueueEntry re =
new RAMQueueEntry(cacheKey, cachedItem, accessCount.incrementAndGet(), inMemory);
if (ramCache.putIfAbsent(cacheKey, re) != null) {
return;
}
int queueNum = (cacheKey.hashCode() & 0x7FFFFFFF) % writerQueues.size();
BlockingQueue<RAMQueueEntry> bq = writerQueues.get(queueNum);
boolean successfulAddition = false;
if (wait) {
try {
successfulAddition = bq.offer(re, DEFAULT_CACHE_WAIT_TIME, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
successfulAddition = bq.offer(re);
}
if (!successfulAddition) {
ramCache.remove(cacheKey);
failedBlockAdditions.incrementAndGet();
}
所以 failedBlockAdditions 是写缓存失败次数,具体是写入 write 队列失败的次数。
问题1.1 :为什么会写入队列失败?有何优化的方法?
查看 ArrayBlockingQueue offer() 方法源码,当队列满了,等待了 timeout 时间后,队列依然是满的,返回 false,入队列失败。
所以 failedBlockAdditions 一般发生在出现大量请求,导致写缓存繁忙的时候。与之的对应的参数有两个:
- write 线程个数:hbase.bucketcache.writer.threads (默认3)
- 每个write 线程队列长度配置:hbase.bucketcache.writer.queuelength(默认32)
适当调大这两个参数可以缓解此问题。
问题2:什么是 hitCachingCount?与 hits 有什么区别?hitRatio 与 cachingHitsRatio 的区别?
hits 是中的缓存命中次数。
hitCachingCount 统计总的缓存命中次数,但排除显示设定不会更新缓存的请求(setBlockCache(false)),它是 HBASE-2253 新加的统计指标,代码如下。目的是为了更精确的统计显示使用缓存的命中率。
/**
* The number of getBlock requests that were cache hits, but only from
* requests that were set to use the block cache. This is because all reads
* attempt to read from the block cache even if they will not put new blocks
* into the block cache. See HBASE-2253 for more information.
*/
private final AtomicLong hitCachingCount = new AtomicLong(0);
这里需要了解一下 hbase 的 setBlockCache 配置。
所有的读请求,都会先请求缓存。默认配置下,读请求(get、scan),如果未命中缓存会更新数据到缓存。
但有时候为了性能优化,针对 mr 或者大 scan 用户会设置不缓存数据。设置方式为:scan.setBlockCache(false)。注意,即使设置了 setBlockCache = false ,读请求第一时间也会先查缓存。
所以 hitCachingCount 是统计总的缓存命中次数,但排除显示设定不会更新缓存的请求。
举个例子:
access: 100 次请求,总命中缓存次数 hits:90 次 hitRatio = 90%
其中 cachingAccesses = 80, hitCachingCount = 78,cachingHitsRatio=78/80 = 97.5%
access =100,cachingAccesses = 80,其中有 20 次请求来自于非缓存请求,例如 mr 或者大 scan 请求。
所以,用户关心的真是缓存命中率为: 97.5%,而不是 90%。
问题3 :IOhitsPerSecond、IOTimePerHit 是什么?
这个是读缓存相关性能指标:
- IOhitsPerSecond: 每秒 bucket io hit 次数
- IOTimePerHit :每次 IO 的耗时
通过源码了解到,读缓存的流程:
- 首先从 RAMCache 中查找。对于还没有来得及写入到 bucket 的缓存 block,一定存储在RAMCache 中;
- 如果在 RAMCache 中没有找到,再在 BackingMap 中根据 blockKey 找到对应物理偏移地址offset;
- 根据物理偏移地址 offset 可以直接从内存中查找对应的 block 数据;
第一步,查找 RAMCache,它是一个 ConcurrentMap,如果命中相当于直接命中内存。
第二步,通过 ioEngine 去读 bucketcache 缓存,所以通过这一步命中的缓存会记录 IO 耗时,这里的 IO 耗时包括:通过 Bytebuffer 进行数据读取、读取后的数据反序列化耗时。
问题4:eviction 是什么?什么时候会发生 eviction?
eviction 字面意思是驱逐,当缓存使用空间 > acceptableSize()或 一种类型的 block 无法申请的时候,需要释放缓存空间,通过 LRU 算法淘汰,驱逐 block。
acceptableSize = bucketc ache 配置总大小*0.95
Free the space if the used size reaches acceptableSize() or one size block
couldn't be allocated. When freeing the space, we use the LRU algorithm and
ensure there must be some blocks evicted
问题 4.1: evictions=14844, evicted=1384179287, evictedPerRun=93248.40625 分别是什么?
eviction:发生 evication 的次数
evicted:总的 evict block 的个数
evictedPerRun :evicted/eviction
问题 5 :bucket cache size 怎么配置的? 使用情况如何?
bucket cache size 配置的分布,从小到大是:(4+1)K,(8+1)K ...(64+1)K 到 (2048+1)K
通过 HBase UI 查看 bucketcache 分布情况,当前申请的 bucket cache size 都是 65K,并且很多 bucket 存储使用率并不高,说明 block 大都是小数据。
问题 5.1 :为什么申请的大都是 (64+1)k 的 bucket ?有什么优化点
这里由于 HBase 默认的 block size 是 64K,说明读请求数据大小大多在 64K block 内。并且,很多 bucket 存储使用率并不高,说明 block 大都是小数据。
这里有什么可以优化的点呢?
可以尝试将表的 blocksize 调小。例如 32k。
参考 HFile 代码注释,HBase 推荐的大小是 8K-1M。设置大有利于顺序扫描,比如 Scan, 但不适合随机查询,想想看,每一次随机查询可能都需要你去解压缩一个大的数据块。
小的数据块适合随机的查询,但是需要更多的内存来保存数据块的索引(Data Index),而且创建文件的时候也可能比较慢,因为在每个数据块的结尾我们都要把压缩的数据流 Flush 到文件中去(引起更多的 Flush 操作)。并且由于压缩器内部还需要一定的缓存,最小的数据块大小应该在 20KB – 30KB 左右。设置小有利于随机读。