1 WAL优化
一个Region有一个WAL实例,WAL实例启动后再内存中维护了一个ConcurrentNavigableMap,是一个线程安全的并发集合,包含了很多个WAL文件的引用,当一个WAL文件写满之后就会开始下一个文件,WAL文件数量不断增长知道达到一个阈值之后开始滚动。相关的优化参数有:
#Region中最大的WAL文件数量,默认值32(当前版本已舍弃)
hbase.regionserver.maxlogs
#HDFS块大小,没有默认值,如果不设置,则使用 hdfs的块大小
hbase.regionserver.hlog.blocksize
#WAL文件大小因子,每一个WAL文件大小通过hdfs块大小*WAL文件大小因子得出,默认0.95
hbase.regionserver.logroll.multiplier
1.1早期优化方式
早期WAL文件数量超过了maxlogs阈值就会引发WAL日志滚动,旧的日志会被清理掉,maxlogs的设置公式为:
(regionserver_heap_size * memstore fraction) /(logRollSize)
(REgionServer堆内存大小*memstore在JVM的堆内存中占的比例)/单个WAL文件的大小
WAL单个文件大小通过blocksize*影响因子。因为大多数人不知道这个公式,使用默认值导致很容易就超过32个log,造成很多不便。后来hbase将maxlogs的数值有HBase内部自己计算得出
1.2 新的优化方式
HBase内部自己的计算方式为:
Math.max(32,(regionserverHeapSize * memstoreSizeRatio *2 /logRollSize))
公式与之前的相差不大,只是分母多了个2倍
2 BlockCache的优化
一个RegionServer只有一个BlockCache,BlockCache不是存储的必要部分,只是用来优化读取性能的,读取数据的时候,client会先从zk获取Region信息,然后到RegionServer上查询BlockCache是否有缓存数据,之后采取memstore和Region中查询,如果获取到了数据,返回同同事会将数据缓存一份到BlockCache。
BlockCache默认是开启的,可以通过如下方式来关闭
alter 'mytable',CONFIGURATION =>{NAME =>'myCF',BLOCKCACHE => 'false'}
BlockCache的实现方案有四种:
2.1 LRUBlockCache
0.92版本之前只有该方法,Least Recently Used,近期最少使用算法的缩写。模仿分代垃圾回收算法,LRU分为三个区域:
- single-access:单次读取区,占比25%,block读出后先放到该区域,当被读取多次后升级到下一个区域
- multi-access:多次读取区,占比50%,当一个被缓存到单次读取区又被多次访问,就会被升级到该区域
- in-memroy:放置设置为IN-MEMORY=true的列族读取出来的block,占比25%。对应列族Block一开始就被放到了in-memory中,不经过单次读取和多次读取区域,并且具有最高存活时间,淘汰Block时候,这个区域Block最后被考虑到。
BlockCache无法关闭,只能调整大小,参数为:
hfile.block.cache.size LRUBlockCache:占用堆内存的比例,默认0.4
配置需要注意的是,Memstor+BlockCache占用的堆内存总量不能超过0.8,否则会报错,留下20%作为机动空间,而Memstore的默认内存也是0.4,所以如果往大调整任何一个值,都必须调小另一个值。
BlockCache有很多优势,通过内存做缓存调高读取性能,但是BlockCache完全基于JVMHeap会导致随着内存中的对象越来越多,每隔一段时间都会引发FULLGC。而为了避免FullGC,出现了基于堆外内存的BlockCache方案。
2.2 SlabCache(已废弃)
所谓堆外内存(off-heap memory),就是不属于JVM管理的内存范围,也就是隶属于原始内存的区域,堆外内存大小可以通过如下设置:
-XX:MaxDirectMemsorySize=60M
堆外内存最大的好处就是JVM几乎不会停顿,也不用害怕回收时候业务卡住,但是堆外内存有几点比较大的缺点:
- 堆外内存存储的数据都是原始数据,如果是一个对象,比如序列化之后才能存储,所以不能存储太大太复杂的对象
- 堆外内存并不是在JVM的管理范围,所以当内存泄漏的时候很不好排查问题
- 堆外内存由于用的是系统内存,使用太多可能导致物理内存溢出,或者因为开启了虚拟内存导致了和磁盘的直接交互
SlabCache调用了nio的DirectByteBuffers,按照8:2的比例划分为两个区域:
- 80%:存放约等于一个BlockSize默认值的Block(64k)
- 20%:存放约等于两个BlockSize默认值的Block(128K)
所以如果数据块大于128K将不会被放入ScabCache中,同时SlabCache也是用LRU算法对缓存对象进行淘汰。所以有时候自定义了BlockSize后,会导致SlabCache放不进去数据,所以HBase使用了LRU和Slab组合的方式:
- 当一个Block被取出的时候同时放到SlabCache和LRUCache中
- 读请求的时候,先查看LRUCache,如果没有采取SlabCache中,如果查到了,就把Block放到LRUCache中。
感觉ScabCache就是LRUCachbe的二级缓存,所以管这个方案中的LRUCache为L1Cache,SlabCache为L2Cache。但实际上SlabCache对blockSize值定的太死,大部分请求还是直接走了LRU,所以对FullGC改善不明显,后来被废弃了。
2.3 BucketCache(同样适用堆外内存)
BucketCache是阿里工程师设计的,借鉴了SlabCache,并且有了改善,
2.3.1 BucketCache的特点
- BucketCache上来直接分配了14种区域,注意是14种,大小分别是4、8、16、32、40、48、56、64、96、128、192、256、384、512KB的block,且种类列表可以通过hbase.bucketcache.bucket.sizes属性来定义(种类之间用逗号分隔,不一定是14个)
- BucketCache存储不一定使用堆外内存,可以使用堆(heap)、堆外(offheap)、文件(file)。可以通过hbase.bucketcache.ioengine为上述三个单词中的一个做配置
- 每个Bucket的大小上限为block4,比如设置最大的Block类型为512KB,那么每个Bucket最大为512KB4=2018KB
- 要保证每一个block类型至少有一个Bucket的空间,否则直接报错,按照每个Bucket的大小上限均分为多个Bucket,之前的例子至少要保证2048KB*14这么大的存储空间
BucketCache可以自己划分内存空间,自己管理内存空间,Block放进去的时候会考虑offset偏移量,所以内存碎片少,发生GC时间短。
另外,之所以会使用file作为存储介质,是因为SSD硬盘的使用,极大地改进了SlabCache使用率低的问题。
2.3.2 BucketCache的相关配置
BucketCache默认是开启的,可以使用如下配置关闭某个列族的BucketCache
hbase > alter 'table_name' CONFIGURATION => {C1 => 'true'}
意思是只使用一级缓存(LRUCache),不使用二级缓存。
BucketCache相关配置有:
#使用的存储介质,可以为heap/offheap/file,默认offheap
hbase.bucketcache.ioengine
#是否打开组合模式,默认为true
hbase.bucketcache.combinedcache.enabled
#BucketCache所占大小,0.0-1.0代表站堆内存比例,大于1的值表示MB为单位的内存;默认为0.0,即关闭BucketCache
hbase.bucketcache.size
#定义所有的Block种类,单位为B,每一种类型必须是1024的整数倍
hbase.bucketcache.bucket.sizes
#该值不是在hbase-site.xml中配置,而是一个启动参数,默认按需获取堆外内存,如果配置了,就相当于设置了堆外内存上限
-XX:MaxDirectMemorySize
2.4 组合模式
在实际生产情况下,虽然BucketCache有很多好处,但是LRUCache性能远比BucketCache强,因为这些二级Cache从速度和可管理性上始终无法和完全基于内存的LRUCache相比。BucketCache和LRU也并不是简简单单的一二级缓存集合,而是称为组合模式,把不同类型的数据分别放到不同的存储策略中。
Index Block会放到LRUCache中,Data Block放到了BucketCache中,所以查询请求在BlockCache阶段会先查询一下LRUCache,会先到LRU查询,然后到BucketCache中查询出真正的数据。数据从一级缓存到二级缓存最后到硬盘,数据从小到大,存储介质由快到慢,比较符合成本和性能规划,而比较合理的存储介质使用是:LRU用内存->BucketCache用SSD->HFile使用磁盘。
3 Memstore的优化
首先大家要明白,Memstore虽然是将数据存储在内存中,但并不是为了加快读取速度,而是为了维持数据结构。HDFS文件不支持修改,为了维持HBase中的数据是按照rowkey顺序来存储的,所以使用Memstore先对数据进行整理,之后才持久化到HDFS上。
3.1 Memstore的刷写机制
Memstore的数据刷写又叫flush,有5种情况会触发刷写。
3.1.1 大小达到了刷写阈值
当单个Memstore占用的内存大小达到了hbase.hregion.memstore.flush.size的配置大小后就回触发一次刷写,默认128MB,生成一个HFile文件,因为刷写是定期检查的,所以无法及时的在数据到达阈值的一瞬间就触发刷写,有时候数据写入非常快,会导致在检查时间还没到之前,Memstore的数据量就达到了刷写阈值的好几倍,就会导致触发写入阻塞机制,此时数据无法写入Memstore,只能进行刷写,影响比较严重。而阻塞机制有个阀值,定义如下:
hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier
hbase.hregion.memstore.flush.size是刷写阀值,默认134217728=128MB
hbase.hregion.memstore.block.multiplier 是一个倍数,默认为4
也就是如果达到了刷写阀值的4倍就会导致刷写阻塞,会阻塞所有写入该Store的写请求,主要是为了应对数据如果继续急速增长导致的更加严重的后果。在解决刷写阻塞的时,并不能一味调大阻塞阀值,而是要综合考虑HFile的相关参数设置。
3.1.2 RegionServer内部所有Memstore的总和达到了阀值
具体的阀值计算公式为:
globalMemStoreLimitLowMarkPercent* globalMemStoreSize
globalMemStoreLimitLowMarkPercent是全局memstore刷写下限,是一个百分比,范围0.0-1.0,默认0.95
globalMemStoreSize是全局memstore的容量,其计算方式为RegionServer的堆内存大小*hbase.regionserver.global.memstore.size,默认0.4,也就是堆内存的40%
总体来说,如果全局Memstore的总和达到了分配给Memstore最大内存的95%,就会导致全局刷写,默认有40%的内存会分给Memstore。而当超过了Memstore的最大内存,也就是堆内存的40%就会触发刷写阻塞。如果有16G堆内存,默认情况下:
#达到该值会触发刷写
16*0.4*0.95=0.608
#达到该值会触发刷写阻塞
16*0.4=6.4
3.1.3 WAL的数量大于maxLogs
当WAL的数量大于maxLogs的时候,也会触发一次刷写,会报警一下,该操作一定不会导致刷写阻塞。
maxLogs的计算公式为:
Math.max(32,(regionserverHeapSize * memstoreSizeRatio * 2 / logRollSize))
该操作主要为了腾出内存空间。
3.1.4 Memstore达到刷写时间间隔
默认时间为1小时,如果设置为0则关闭自动定时刷写。配置参数为:
hbase.regionserver.optionalcacheflushinterval 默认为3600000,1个小时
3.1.5 手动触发刷写
JavaAPI Admin接口提供的方法为:
#对单个表刷写
flush(TableName tableName)
#对单个Region刷写
flushRegion(byte[] regionName)
hbase shell命令:
#单表刷写
flush 'tableName'
#单个Region刷写
flush 'regionname'
3.2 总结
memStore的优化可以主要关注放置触发阻塞机制了,要合理设置Memstore的内存,以及RegionServer的堆内存。