- 本篇文章不是自己总结的,而是参考资料上进行摘抄的,本文涉及到HBase的Region拆分合并以及HFile拆分合并以及其他的调优参考,如果你参考本文的话,请一定在尝试之后在修改配置,本文只是自己的一个学习记录方便以后查阅
- 参考了 HBase权威指南 和 HBase不睡觉书
尽信书不如无书,在使用的时候需要先测试!!!!!
调大堆内存
- 默认RegionServer的内存是1GB,而MemStore默认是占百分之四十,所以MemStore才有400MB空间,在实际应用中,很容易就会被写阻塞了,可以通过指定HBASE_HEAPSIZE参数来调整所有HBase实例占用的内存大小,不管是Master还是RegionServer
#在hbase-env.sh中
# The maximum amount of heap to use. Default is left to JVM default.
export HBASE_HEAPSIZE=4G #原来是1G,现在修改为4G
- 上面参数会影响整个HBase实例,包括master和region,这样的话master和RegionServer都会占用4GB
设置Master和RegionServer的内存大小
export HBASE_MASTER_OPTS="$HBASE_MASTER_OPTS -Xms4g -Xmx4g"
export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g"
- 上面是将master的JVM内存设置为4G,RegionServer的内存设置为8G,但是你需要留出内存的百分之十给操作系统
- 参考:16G的机子上有RegionServer,MR,DN服务,那么2G需要给系统,8G给MR,4G给RegionServer,1GDN,1GtaskTracker
Full GC
- 随着堆内存越大,那么Full GC回收的时间就越长,在Full GC的时候JVM会停止响应任何请求,这种暂停被称为STW(stop-the-world)
- 当zk像往常一样通过心跳来检测RegionServer节点是否存活的时候,发现已经很久没有来自RegionServer的回应,会直接将这个RegionServer标记为宕机,等到这台RegionServer结束Full GC后回来,去汇报ZK的时候,发现自己已经挂了,为了防止脑裂问题的发生,只能自己停止自己,这种场景被称为RegionServer自杀(朱丽叶暂停),所以我们要设定好GC的回收策略,避免长时间的Full GC,或者是尽量减少Full GC时间
GC回收策略优化
- 数据都存放在RegionServer上,所以问题一般出在RegionServer上,Master只是做一些管理操作
-
JVM的4种GC回收器
- 串行回收器SerialGC
- 并行回收器ParallelGC,主要针对年轻代进行优化(jdk8默认策略)
- 并发回收器ConcMarkSweepGC,CMS,主要针对年老带进行优化
- G1GC回收器,针对大内存进行优化(内存>=32)
-
ParallelGC和CMS的组合方案
- 并行回收器比串行的Full GC时间较短,而Full GC对于RegionServer的影响是很大的,所以我们采用并行回收器
- 并发回收器主要是减少老年代的暂停时间,可以保证应用不停止的情况下进行收集,缺点是每次都会落下一些"浮动垃圾",这些浮动垃圾只能在下次垃圾回收的时候被回收,这个是可以忍受的
- 所以基于上面的描述,HBase配置是年轻代使用并行回收器,年老代使用并发回收器
- 修改
#habse-env.sh export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g -XX:+UseParNewGCXX:+UseConcMarkSweepGC"
-
G1GC方案
-
这个方案需要你的RegionServer的内存大于4GB并且大于JDK1.7.0_04版本,因为这是jdk这个版本新加入的策略,这个方案适用于堆内存很大的情况,引入G1GC策略的原因是:就算采用CMS策略,还是不能避免FullGC,因为两种情况还是会引发CMS的Full GC
- 当你的年老代不足的时候,年轻代又在不断的移到年老代的时候,就会引发Full GC
- 当被回收掉的内存空间太碎太细小,导致新加入年老代的对象放不进去,就会触发Full GC整理空间
- G1GC通过把对内存划分为多个区域,然后对各个区域单独进行GC,这样整体的Full GC可以被最大程度的避免,这种策略还可以通过手动指定MaxGCPauseMillis控制一旦发生Full GC的时候的最大暂停时间,避免时间太长造成RegionServer自杀
- 修改
export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=100"
-
32G,64G才算是大内存,当内存在4G或者32G之间的时候,需要测试方案的可行性来决定采用哪种方案,下面是一些参考意见
- 如果RegionServer内存小于4GB,就不需要参考G1GC策略,直接采用并行加并发策略
- 如果RegionServer内存大于32GB,就推荐G1GC策略
- 在测试的时候把调试参数加上,这样就可以看到试验的量化结果
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
-
Memstore的专属JVM策略MSLAB
-
现在内存越来越大,每增加一GB,平均会增加8到10秒的FullGC时间,所以当堆内存很大的时候,Full GC会消耗很多时间,对于CMS算法依旧会触发Full GC,原因是
- 同步模式失败:在CMS还没有把垃圾收集完的时候空间还没有完全释放,在这个时候如果新生代的对象过快的转换为老年代的对象时发现老年代的可用空间不够用了,此时收集器就会停止收集过程,转为单线程的STW暂停,将又触发Full GC了,不过这个过程可以设置:
-XX:CMSInitiatingOccupancyFraction=N
来缓解,N代表了当JVM启动垃圾回收时的堆内存占用百分比,设置的越小,JVM越早启动垃圾回收进程 - 由于碎片化造成的失败:当前要从新生代提升到老年代的对象比老年代的所有可以使用的连续的内存空间都大,比如老年代有500MB的空间可用,但是都是1KB的碎片空间,现在有一个2KB的对象要提升到老年代却发现没有一个空间可以插入,这时就会触发SWT暂停进行FullGC,这种碎片化情况通过设置
-XX:CMSInitiatingOccupancyFraction=N
是不起作用的,因为CMS只做回收不做合并,所以RegionServer启动久了肯定会遇到Full GC
- 同步模式失败:在CMS还没有把垃圾收集完的时候空间还没有完全释放,在这个时候如果新生代的对象过快的转换为老年代的对象时发现老年代的可用空间不够用了,此时收集器就会停止收集过程,转为单线程的STW暂停,将又触发Full GC了,不过这个过程可以设置:
-
为什么会产生碎片化空间
- MemStore是会定期刷写HFile的,在刷写的同时,这块MemStore所占用的空间会被标记为待回收,一旦被回收了,这部分内存就可以再次被使用,但是由于JVM分配对象都是按顺序分配下去的,所以内存空间使用了一段时间后情况就是这样的
假设红色大小为1KB,此时有2KB的新生代升级为老生代,但是JVM找不到连续的2KB内存空间去存放新对象,因为被刷写的内存只是被标记还没有释放,JVM这时候只好停止一切请求,然后将内存空间进行重新排列,这个排列的时间随着内存空间的增大而增大,当内存空间足够大的时候,暂停的时间足以让zk认为RegionServer挂了
- JVM为了避免这个问题有一个基于线程的解决方案,叫TLAB(thread-local allocation buffer),当你使用TLAB的时候,每一个线程都会分配一个固定大小的内存空间,专门给这个线程使用,当线程用完这个空间后再新申请的空间还是这么大,这样就不会出现特别小的碎片空间,基本所有的对象都可以有地方存放,缺点就是无论线程里面有没有对象需要占用这么大的内存,都会开辟这么大,有很多内存都是闲置的,内存空间利用率会降低,依次避免Full GC(即每次申请空间一致,避免申请的不同空间使用后的差值造成碎片)
- 但是HBase并不能使用TLAB,因为HBase中多个Region是被一个线程管理的,多个MemStore占用的空间还是无法合理的分开,于是HBase自己实现了一套以MemStore为最小单元的内存管理机制,称为MSLAB(MemStore-Local Allocation Buffers),这套机制沿袭了TLAB的实现思路,只不过内存空间是由MemStore来分配的
-
MSLAB实现
- 加入chunk,即一块2MB大小的chunk
- RegionServer中维护着一个全局的MemStoreChunkPool实例,是一个chunk池
- 每个MemStore实例里面有一个MemStoreLAB实例
- 每个MemStore接收到KeyValue数据的时间先从ChunkPool中申请一个chunk,然后放到整个chunk里面
- 如果chunk放满了,就重新申请一个chunk
- 如果MemStore因为刷写而释放内存,则按chunk来清空内存
- 由此可以看出堆内存被chunk区分为规则的空间,这样就消除了小 碎片引起的无法插入数据问题,但是会降低内存利用率,因为就算你的 chunk里面只放1KB的数据,这个chunk也要占2MB的大小.整个开销对于Full GC来说还是可以忍的
-
MSLAB相关参数
-
hbase.hregion.memstore.mslab.enabled
:设置为true,即打开 MSLAB,默认为true -
hbase.hregion.memstore.mslab.chunksize
:每个chunk的大 小,默认为2048*1024 即2MB -
hbase.hregion.memstore.mslab.max.allocation
:能放入chunk 的最大单元格大小,默认为256KB,已经很大了 -
hbase.hregion.memstore.chunkpool.maxsize
:在整个memstore 可以占用的堆内存中,chunkPool占用的比例,该值为一个百分 比,取值范围为0.0~1.0。默认值为0.0 -
hbase.hregion.memstore.chunkpool.initialsize
:在 RegionServer启动的时候可以预分配一些空的chunk出来放到 chunkPool里面待使用.该值就代表了预分配的chunk占总的 chunkPool的比例,该值为一个百分比,取值范围为0.0~1.0,默 认值为0.0
-
Region的拆分
- Region就是一段数据的集合,当Region太大的时候就会拆分它,拆分Region的根本的原因是Region大了那么读取效率就低了
-
region自动拆分ConstantSizeRegionSplitPolicy
-
最早的0.94左右版本的时候只有一种拆分策略就是
ConstantSizeRegionSplitPolicy
,即计算Region的到小来达到拆分的目的,用到的参数是<property> <name>hbase.hregion.max.filesize</name> <value>10737418240</value> </property>
- 即如果达到10GB就会将这个Region拆分为两个
-
-
region自动拆分IncreasingToUpperBoundRegionSplitPolicy
- 之后就有了
IncreasingToUpperBoundRegionSplitPolicy
拆分策略,该策略是限制不断增长的文件尺寸,文件尺寸限制是动态的
Math.min(tableRegionCount^3 * initialsize, defaultRegionMaxFileSize)
- 之后就有了
-
- tableRegionCount:表在所有RegionServer上所拥有的Region数量总和
- initiaSize:是
hbase.increasing.policy.initial.size
这个数值,如果没有定义这个属性那么就用memstore的刷写大小的2倍 - defaultRegionMaxFileSize:即Region 的最大大小
- 假如HFile的刷写大小为128MB.当开始只有一个文件的时候文件尺寸上限为1^3*128*2=256MB,当是两个文件件的时候,最大为2^3*128*2= 2048MB,以此类推直到拥有四个文件的的时候,计算出来的值为16GB,已经超过最大Region大小了,所以之后Region数量在增加,文件大小上限也不会增加了
-
region自动拆分KeyPrefixRegionSplitPolicy
- 这个类是前面介绍的子类,在前面拆分策略上增加了拆分点(即Region被拆分处的rowkey),保证了有相同前缀的rowkey拆分到一个region中
KeyPrefixRegionSplitPolicy.prefix_length rowkey:前缀长度
- 该策略会根据定义的长度来截取rowkey作为分组的依据,同一组的数据不会被划分到不同的region上
- 这种策略对于只有一两个前缀的数据,那么根本不需要用这个,这个适用场景在于数据有多种前缀,并且查询多是针对前缀,比较少跨越多个前缀来查询数据
-
region自动拆分DelimitedKeyPrefixRegionSplitPolicy
- 跟前面介绍的是同一个父类,同样是根据rowkey进行切分,不同的是前面介绍的前缀策略是根据rowkey的固定前几个字符来进行判断的,而这个是根据分隔符来判断的,所以这个策略需要的参数就是你定义的rowkey 的分隔符
DelimitedKeyPrefixRegionSplitPolicy.delimiter : 前缀分隔符
-
region自动拆分BusyRegionSplitPolicy
-
一看名字就知道是热点region的拆分策略,那么怎么算是热点呢?其中涉及几个参数
-
hbase.busy.policy.blockedRequests
:请求被 阻塞的严重程度,取值范围是0.0~1.0,默认是0.2,即20%的请求被阻塞的意思 -
hbase.busy.policy.minAge
:拆分最小年龄,当Region的年龄比这个小的时候不拆分,这是为了防止在判断是否要拆分的时候出 现了短时间的访问频率波峰,结果没必要拆分的Region被拆分 了,因为短时间的波峰会很快地降回到正常水平.单位毫秒,默认值10分钟 -
hbase.busy.policy.aggWindow
:计算是否繁忙的时间窗口,单位毫秒,默认值5分钟,用以控制计算的频率
-
-
计算region是否繁忙的计算方法
- 如果
当前时间减去上次检测时间>=hbase.busy.policy.aggWindow
,则进行如下计算:这段时间的阻塞的请求/这段时间的总请求=请求的被阻塞率aggBlockedRate
- 如果
aggBlocedRate>hbase.busy.policy.blockedRequests
,则判断该Region为繁忙
- 如果
- 所以这种策略通过计算,然后拆分热点region来达到分散region的压力,但是根据热点拆分region会造成你不知道下一个被拆分的region是哪个
-
-
region自动拆分DisabledRegionSplitPolicy
- 即代表region永远不拆分,但还是可以通过手动拆分region的,那么他有什么作用呢?
- 当数据进入HBase时肯定是往一个region中放,当达到阀值之后,region就开始拆分,但是如果是巨量数据写入就会造成一边拆一边写的情况,如果事先知道这个表按照什么策略来拆分region的话,那么你就可以使用此策略停止region的拆分,然后手动的去拆分region
-
region预拆分
- 就是在建表的时候就定义好了拆分点的算法,使用
RegionSplitter
类来创建表,并传入拆分点算法就可以在建表的同时定义拆分点算法 - 使用:建立一个表其region数量一致保持是10个
hbase(main):005:0> create 'mysplittable','cf',{NUMREGIONS=>10,SPLITALGO=>'HexStringSplit'}
- NUMREGIONS是指你的region数目,而SPLITALGO是切分算法,扫描meta表,查看region区域
STARTKEY => '', ENDKEY => '19999999' STARTKEY => '19999999', ENDKEY => '33333332' STARTKEY => '33333332', ENDKEY => '4ccccccb' STARTKEY => '4ccccccb', ENDKEY => '66666664' STARTKEY => '66666664', ENDKEY => '7ffffffd' STARTKEY => '7ffffffd', ENDKEY => '99999996' STARTKEY => '99999996', ENDKEY => 'b333332f' STARTKEY => 'b333332f', ENDKEY => 'ccccccc8' STARTKEY => 'ccccccc8', ENDKEY => 'e6666661' STARTKEY => 'e6666661', ENDKEY => ''
- HexStringSplit:就是将00000000到FFFFFFFF之前的数据按照你给定的region与切分数切分为不同region
- UniformSplit:和 HexStringSplit差不多,唯一区别是startkey和endkey不是String了而是byte[]
- 还可以通过实现SplitAlgorithm接口实现自己的拆分算法
- 在hbase shell界面列举出了如何进行预拆分,如下图
- 就是在建表的时候就定义好了拆分点的算法,使用
- region强制拆分
Region的合并
- 当你删除了大量数据,region变小后,为了节省资源成本可以将多个region来进行合并
-
通过Merge类合并
hbase(main):011:0> merge_region '97fe9387f00844770619924108925ab4','4e35b33014d4d1a971059c78268b87ce'
- 两个字符串是来自region名的后缀的
压缩
- HBase支持大量压缩算法,并且可以支持列族上的数据压缩,一般是推荐开启压缩的,因为CPU压缩和解压缩消耗的时间比从磁盘中读取和写入更多数据消耗的时间更短
- 可用编码器:Snappy,LZO,GZIP...
- 对于HBase配置压缩方式可以百度一下
BlockCache优化
- 一个RegionServer只有一个 BlockCache,之前也提到过,在client查询数据的时候,首先去查BlockCache,获取不到再去查HFile和MemStore中获取,如果获取了则在返回数据的同时把Block块缓存到BlockCache中
- BlockCache默认是开启的
-
BlockCache实现方案:LRUBlock Cache
- 完全基于JVM heap,即最少使用算法的缩写,读出来的block会被放入BlockCache中待下次查询使用,当缓存满了的时候,会根据LRU的算法来淘汰block
- LRUBlockCache被分为三个区域
区域名称 | 占用比例 | 说明 |
---|---|---|
single-access | 25% | 单次读取区,block被读出后先放入此区域,当被读到多次后,升级到下一个区域 |
multi-access | 50% | 多次读取区,当一个被缓存到单词读取区后又被多次访问会升级到这个区 |
in-memeory | 25% | 这个区域跟block被访问了几次没有关系,它只存放哪些被设置了IN-MEMEORY=TRUE的列族 中读取出来的block |
- 并不是说列族属性被标记为true,列族就被放入内存,只是说被标记为true的列族中的数据一开始就会被放入in-memory区域,这个区域的缓存有最高的存活时间,在需要淘汰block的时候,这个区域的block是最后被考虑到的
- 调整大小,但是不能关闭
<property>
<name>hfile.block.cache.size</name>
<value>0.4</value>
</property>
- 既然是基于堆内存的,那么势必在会造成Full GC
-
BlockCache实现方案:SlabCache(废弃)`
-
堆内会产生Full GC,就需要考虑堆外内存了,堆外内存就是存放在了内存区域并且不属于JVM管理的,大小可以通过
-XX:MaxDirectMe morySize=60MB
配置,最大的好处就是堆外内存回收的时候JVM几乎不会停顿,但是他也有缺点- 因为在堆外内存存储的数据都是很原始的数据,如果是一个对象,比如先序列化之后才能存储,所以不能存储太复杂的对象
- 堆外内存并不是在JVM管理范围内,所以当内存泄露很不好排查
- 堆外内存直接使用的是系统内存,当使用太大的时候,物理内存会爆掉,如果是虚拟内存,会影响到硬盘的使用
-
SlabCache调用了nio的DirectByteBuffers,它把堆外内存按照80%和20%比例划分为两个区域
- 存放大小约等于一个BlockSize默认值的Block
- 存放大小约等于两个BlockSize默认值的Block
- 一个BlockSize默认是64KB,是定义在列族上的,可以进行修改,如果Block大于这两个范围就放不进入,SlabCache也采用LRU算法进行淘汰缓存对象
- SlabCache允许存放的Block块大小是BlockSize默认值的1倍 和2倍,如果改了列族的BLOCKSIZE属性那这两区域都用不到了,解决办法是当一个Block被取出的时候同时被放到SlabCache和LRUCache中,当读请求到来的时候先查看LRUCache,如果查不到就去查 SlabCache,如果查到了就把Block放到LRUCache中,类似觉SlabCache就是LRUCache的二级缓存,以HBase管 这个方案中的LRUCache叫L1 Cache,管SlabCache叫L2 Cache.不过这个最终是废弃了,因为定值太死,并且推出了一个更好的替代方案
-
-
BlockCache实现方案:Bucket Cache
- 也是使用堆外内存,不过它不止两个区域选择,而是14种区域,并且还可以通过配置
hbase.bucketcache.bucket.sizes
来定义,所以说是可以配置出很多Bucket,14种区域分别为4KB、8KB、16KB、32KB、40KB、 48KB、56KB、64KB、96KB、128KB、192KB、256KB、384KB、 512KB的Block,种类之间用逗号分隔,必须是 1024的整数倍 - 不过它不止使用堆外内存,还可以使用堆,堆外,文件,通过配置
hbase.bucketcache.ioengine
为heap、 offfheap或者file来配置 - 每个Bucket的大小上限为最大尺寸的block * 4,比如可以容纳 的最大的Block类型是512KB,那么每个Bucket的大小就是512KB * 4 = 2048KB
- 系统一启动BucketCache就会把可用的存储空间按照每个Bucket 的大小上限均分为多个Bucket.如果划分完的数量比你的种类还 少,比如比14(默认的种类数量)少,就会直接报错,因为每一 种类型的Bucket至少要有一个Bucket
- 也是使用堆外内存,不过它不止两个区域选择,而是14种区域,并且还可以通过配置
- BucketCache对内存地址的管理:它自己来划分内存空 间、自己来管理内存空间,Block放进去的时候是考虑到offset偏移量 的,所以内存碎片少,发生GC的时间很短
- 其中file是指ssd硬盘
- BucketCache用到的配置项如下
alter 'test',CONFIGURATION=>{CACHE_DATA_IN_L1 => 'true'}
- 意思是只缓存在一级缓存(LRUCache)中,不使用二级缓存 (BucketCache)
-
相关配置如下
-
hbase.bucketcache.ioengine
:使用的存储介质,可选值为 heap、offheap、file.默认为offheap。 -
hbase.bucketcache.combinedcache.enabled
:是否打开组合模 式(CombinedBlockCache)默认为true -
hbase.bucketcache.size
:BucketCache所占的大小
-
- 如果设置为0.0~1.0,则代表了占堆内存的百分比.如果是大于1的值,则代表实际的BucketCache的大小单位为MB.默认值为0.0,即关闭BucketCache
-
-XX:MaxDirectMemorySize
:这个参数是JVM启动的参数,如果不配置这个参数,JVM会按 需索取堆外内存,如果配置了这个参数,你可以定义JVM可以 获得的堆外内存上限,这个参数值必须比hbase.bucketcache.size
大
-
组合模式
- 说就是把不同类型的Block分别放到 LRUCache和BucketCache中
- Index Block和Bloom Block会被放到LRUCache中。Data Block被直 接放到BucketCache中,所以数据会去LRUCache查询一下,然后再去 BucketCache中查询真正的数据。其实这种实现是一种更合理的二级缓 存,数据从一级缓存到二级缓存最后到硬盘,数据是从小到大,存储介 质也是由快到慢。考虑到成本和性能的组合,比较合理的介质是: LRUCache使用内存->BuckectCache使用SSD->HFile使用机械硬盘
MemStore的优化
- 所有的写操作在写入到磁盘上之前都会先放到MemStore上,直到MemStore被刷写到磁盘上,数据才真正的持久化到硬盘上,如果开启了BlockCache,那么读取数据的时候会先查询BlockCache,当发现没有的时候,才会去查询MemStore+HFile的数据,由于有些数据还未被刷写到HFile中,所以MemStore+HFile才是所有数据的集合,MemStore的目的是为了维持数据结构而并非加速写入,因为HDFS文件不支持修改,为了维持HBase数据的顺序,所必须先对数据进行整理后再持久化到HDFS上
- MemStore的刷写
- 数据进入HBase,在写入硬盘之前,会先暂存在内存中,数据不能一直驻留在内存,当数据从内存写到硬盘上的时候,这个动作叫做MemStore的刷写
-
什么时候会触发刷写机制?
- 大小达到刷写阀值:占用内存大小超过hbase.hregion.memstore.flush.size配置值就会导致刷写,默认是128MB,即HDFS块大小,刷写就会生成一个HFile.因为刷写是定期检查的,所以无法及时的在数据达到阀值的时候触发刷写,如果数据增长太快,而且超过了配置值,这时候就会触发阻塞机制,造成无法写入数据到MemStore
- 阻塞机制:
<property> <name>hbase.hregion.memstore.block.multiplier</name> <value>4</value> </property> <property> <name>hbase.hregion.memstore.flush.size</name> <value>134217728</value> </property>
- 默认的阻塞阀值是上面两个配置项的乘积,如果在下一次刷写检查到来之前就达到了这个阀值,就会立刻触发一次刷写,这次刷写不仅仅是内存中的数据了,并且还会在刷写的时候同时阻塞所有写入Store的写请求.所以如果遇到了这的性能问题,可以调大刷写值,但是要同时参考HFile的相关参数设置,之后会说
- 整个RegionServer和memstore总和达到阀值
- 阀值计算
hbase.regionserver.global.memstore.size.lower.limit全局memstore刷写下 限以取值范围在0.0~1.0,默认为0.95 * hbase_heapsize(RegionServer占用的堆内存大小)* hbase.regionserver.global.memstore.size
- 比如
hbase.regionserver.global.memstore.size.lower.limit=0.95 hbase_heapsize* hbase.regionserver.global.memstore.size=16G*0.4 触发刷写阀值为:16*0.4*0.95=6.08,触发阻塞阀值为:16*0.4=6.4G 所以当memstore到达6.08G会触发强制刷写,当达到6.4G时会阻塞HBase集群的写入
- WAL的数量大于maxLogs
maxLogs计算公式: Math.max(32,(regionserverHeapSize * MemStoreSizeRatiio * 2 / logRollSize))
- 当wal数量大于这个值就会触发memstore刷写以便创造新的memstore内存空间用来加载WAL中的数据,同时HBase会给出一个info日志,只是提醒你达到wal滚动条件了
Too many wals,counts=34,max=32,forcing flish of...
- MemStore达到刷写时间间隔
<property> <name>hbase.regionserver.optionalcacheflushinterval</name> <value>3600000</value> </property>
- 默认1小时,如果设定为0,则意味着关闭定时自动刷写
- 手动触发flush:admin类和hbaseshell都有对应的操作方法flush xxx
HFile的合并
- HFile经常合并的原因是,HFile存储在硬盘上,硬盘需要寻址,HFile一多,性能势必下降,为了防止浪费更多的寻址时间,我们就需要合并操作
- HFile合并操作就是在一个Store里面找到需要合并的HFile,然后把他们合并起来,最后移除碎片文件
HFile合并策略
-
Minor Compaction和Major Compaction
- Minor:将Store里的HFile合并为一个HFile.删除TTL数据,但是不会删除手动删除的数据,触发率较高
- Major:将Store里的HFile合并为一个HFile.删除TTL,并且删除手动删除数据和超过maxVersion的版本数据,触发率较低
- Major Compaction只是把一个Store中的HFile合并为一个HFile
-
0.96之后的合并策略ExploringCompactionPolicy
- 是把所有的HFile都遍历一遍之后才会去考虑合并哪个HFile,符合合并条件的计算公式是
该文件 < (所有文件大小总和 - 该文件大小) * 比例因子
- 如果该文件大小小于
hbase.hstore.compaction.min.size
,即最小合并大小,那么就直接进入待合并状态不需要套用公式,如果hbase.hstore.compaction.min.size
没有配置就使用刷写大小hbase.hregion.memstore.flush.size
限制,如果满足公式或者配置项任何一个就会进入待合并状态 - 被挑选的文件除了达到公式和最小合并大小的任一要求外,还需要组合内包含的文件数必须大于于hbase.hstore.compaction.min,小于 hbase.hstore.compaction.max,组合即HFile构成的集合
- 挑选完组合后,比较那个文件组合包含的文件更多,就合并哪个组合,如果出现平局,就筛选文件尺寸总和更小的组合
- 默认hbase.hstore.compaction.max=10 , hbase.hstore.compaction.min = 3
- 比方说有十个HFile,其中有5个满足条件被标记为待合并,因为我们的
hbase.hstore.compaction.min
为3,设hbase.hstore.compaction.max
=4,所以5个HFile就进行排列组合,比如12345五个HFile,会组成
123 1234 1235 125 ...
- 每个组的最小HFile数量不能小于3,并且不能大于5,之后就找到HFile文件最多的一组,就开始合并,如果出现了两组一样多HFile文件的组,就比较两组的总大小,哪个组小就合并哪个,即多文件小尺寸优先合并
-
FIFOCompactionPolicy
-
FIFOCompactionPolicy策略在合并时会跳过含有未过期数据的 HFile,直接删除所有单元格都过期的块,最终
- 过期的块被整个删除掉了
- 没过期的块完全没有操作
- 合并的时候不会合并超时的数据,所以如果要合并的两个块的所有数据都过期了,那合并这两个块的操作其实就是把他们都删除了而已
- 这个策略不能用于表没有设置TTL的情况或者TTL=FOREVER或者设置了MIN_VERSION并且MIN_version>0的情况
- 当版本达到TTL需要被删除的时候会先看一下单元格里面的版本数 是不是等于MIN_VERSIONS,如果是的话就放弃删除操作。所以,如果有 MIN_VERSIONS在,TTL就会失效,所以不适用FIFOCompactionPolicy
- Major Compaction删除的是那些带墓碑标记的,而 Minor Compaction合并的时候直接就不读取过期的文件,所以过期的这 些文件会在Minor Compaction的时候就被删除
-
-
DateTieredCompactionPolicy
- DateTieredCompactionPolicy解决的是一个基本的问题:最新的数据最 有可能被读到
- 应用场景举例:即发朋友圈,新发的朋友圈是最有可能被别人看的,而发表很长时间之后的朋友圈,基本没人阅读,所以朋友圈的HFile就会大概分为两种一个新发表的数据和旧发表的数据,而这个策略就是将新的和新的合并,旧的再一起合并,并且数据是改动不频繁的
- 配置
hbase.hstore.compaction.date.tiered.base.window.millis: 基本的时间窗口时长,默认是6小时。即从现在到6小时之内的HFile都在同一个时间窗口里 面,即这些文件都在最新的时间窗口里面,类似"时间分区" hbase.hstore.compaction.date.tiered.windows.per.tier:层 次的增长倍数。分层的时候,越老的时间窗口越宽 hbase.hstore.compaction.date.tiered.max.tier.age.millis: 最老的层次时间。当文件太老了,老到超过这里所定义的时间范 围就直接不合并了
- 比如举个例子
基本窗口宽度 (hbase.hstore.compaction.date.tiered.base.window.millis) = 1 最小合并数量(hbase.hstore.compaction.min) = 3 层次增长倍数 (hbase.hstore.compaction.date.tiered.windows.per.tier) = 2
- 把当前Store中的HFile从旧到新排列,然后画上时间窗口的分界 线,就变成如图
- 可以看到只有第三个时间窗口中的HFile数量达到了最小合并数量即3个所以第三个时间窗口中 的文件会被合并
- 上面图是正好避开时间点的,如果一个HFile跨越时间线,那么HFile是计入更老的窗口的,比如
- 这种策略如果达到合并条件,使用的合并策略是之前提到的一种,默认为ExploringCompactionPolicy
-
这种策略适用于
- 经常读写最近数据的系统,或者说这个系统专注于最新的数据
- 更适用 于那些基本不删除数据的系统
- 数据根据时间排序存储
- 数据的修改频率很有限,或者只修改最近的数据,基本不删除数 据
- StripeCompactionPolicy
- 如上图,当memstore刷写HFile的时候首先刷到L0区,当L0达到一定阀值的时候,就会将L0读出来放入Strips区域,Strips内的区域个数是可以自定义的,区域是按照行键划分数据的.
-
它解决了什么问题?
- 通过增加L0层,等于增加了一层缓冲,让合并更缓和
- 严格按照行键划分Strips,可以提高查询速度的稳定性
- Strips的各个区域可以单独的执行Major Compaction,比如a-f执行合并而g-m不受影响
- 这个策略特点是稳定
-
适用于什么场景?
- Region要大,这种策略实际上就是把Region给细分成一个个 Stripe,Region大小小于2G不考虑
- Rowkey要具有统一格式,能够均匀分布,因为是按照行键划分的
compaction的吞吐量限制参数
- 由于compaction机制会造成HBase出现IO突然降低的情况,但是compaction本身又是不可或缺的,没有 compaction性能更差,而且被删除的数据还不能真正清除,HBase就提供了一个简单的处理方案:通过配置来限制compaction时 占用的IO性能
-
参数设置
hbase.regionserver.throughput.controller:你要限制的类型 对应的类名 控制合并相关指标: PressureAwareCompactionThroughputController:默认 控制刷写相关指标: PressureAwareFlushThroughputController hbase.hstore.blockingStoreFiles:当StoreFile数量达到该 值,阻塞刷写动作 hbase.hstore.compaction.throughput.lower.bound:合并占用吞吐量下限 hbase.hstore.compaction.throughput.higher.bound:合并占用吞吐量上限
-
hbase.hstore.blockingStoreFiles
- StoreFile阻塞值,默认16,当Store中的HFile数量达到16的时候阻塞Memstore的刷写
- 所以当memstore内存急剧增加的情况下,就应该考虑这种情况,memstore达到阻塞值,被阻塞后致使不能刷写,所以memstore达到了他的写入上限
memstore写入上限:hbase.hregion.memstore.block.multiplier*hbase.hregion.memstore.flush.size
-
合并/刷写吞吐量限制机制
-
HBase会计算合并/刷写时占用的吞吐量,然后当占用吞吐量过大的 时候适当地休眠,限制机制是分高峰时段和非高峰时段的,通过两个参数配置
hbase.offpeak.start.hour:每天非高峰的起始时间,取值为 0~23的整数,包含0和23 hbase.offpeak.end.hour:每天非高峰的而结束时间,取值为 0~23的整数,包含0和23
- 在非高峰期是不限速的,只有在高峰期当合并/刷写占用了太大的 吞吐量才会休眠,决定是否要休眠是看当时占用的流量是否达到休眠吞 吐量阀值,休眠吞吐量阀值的计算公式是
lowerBound:hbase.hstore.compaction.throughput.lower.bound合并占用吞 吐量下限,默认是10 MB/sec upperBound:hbase.hstore.compaction.throughput.higher.bound合并占用 吞吐量上限,默认是20 MB/sec pressureRatio:压力比。限制合并时,该参数就是合并压力compactionPressure,限制刷写时该参数刷写压力 flushPressure这个数值为0~1.0 公式:lowerBound + (upperBound - lowerBound) * pressureRatio
- 当达到吞吐量阀值的时候合并线程就会sleep一段时间,为业务响 应留出足够的吞吐量,保证业务响应的流畅度和保证系统的稳定性
-
-
压力比
- 压力比(pressureRatio)越大,代表HFile堆积得越多,或者即将产生越多的HFile,一旦HFile达到阻塞阈值,则无法写入任何数据,系 统就不可用了.所以合并压力越大,代表着系统不可用的可能性越大
- 此时合并的需求变得迫在眉睫,我们需要分配更多的吞吐量给合并操 作。从公式中可以看出,当压力比越大的时候,吞吐量阈值就越高,意 味着合并线程可以占用更多的吞吐量来进行合并。
- 当presssureRatio的计算结果大于1.0了,说明压力太大了,在不合并集群将不能工作了,所以此时阀值是取消的,即不限制合并的吞吐量
- 压力比,分为合并压力(compactionPressure)和刷写压力 (flushPressure)两种
- compactionPressure是怎么计算出来的
(HfileCount - minFilesToCOmpact) / (blockingFileCount - minFilesToCOmpact) HfileCount:当前storeFile数量 minFilesToCOmpact:单词合并文件数量下线,即hbase.hstore.compaction.min blockingFileCount:即hbase.hstore.blockingStoreFiles
- 所以可以看出当前的HFile越大或者阻塞上 限越小,那么合并的压力就越大,因为更有可能发生阻塞
- 如果HFile数量比单次合并文件数量下限还小,说明绝对不会发生合并,此时CompactionPressure=0
- flushPressure是怎么计算出来的
globalMemstoreSize / memstoreLowerLimitSize globalMemstoreSize:当前的memstore大小 memstoreLowerLimitSize:memstore刷写下线,当全局 memstore达到这个内存占用数量的时候就会开始刷写
- 从上面可以看出如果当前Memstore占用的内存越大,或者刷写的触 发条件越小,越有可能引发刷写。发生刷写后,HFile的数量就会增 多,即越有可能因为HFile过多触发阻塞
合并的时候HBase做了什么
-
获取需要合并的HFile列表
- 获取列表的时候需要排除掉带锁的HFile。锁分两种:写锁(write lock)和读锁(read lock)。当HFile正在进行以下操作的时候会上 锁
用户正在scan查询:上Region读锁(region read lock) Region正在切分(split):此时Region会先关闭,然后上 Region写锁(region write lock) Region关闭:上Region写锁(region write lock) Region批量导入:上Region写锁(region write lock)
-
由列表创建出StoreFileScanner
- HRegion会创建出一个Scanner,用这个Scanner来读取本次要合并 的所有StoreFile上的数据
-
把数据从这些HFile中读出,并放到tmp目录
- HBase会在临时目录中创建新的HFile,并使用之前建立的Scanner 从旧HFile上读取数据,放入新HFile。TTL到期和墓碑数据不会被读取出来
-
用合并后的HFile来替换合并前的那些HFile
- 最后用临时文件夹内合并后的新HFile来替换掉之前的那些HFile文 件
Major Compaction
- Minor Compaction的目的是增加读性能,而majorCompaction在 minorCompaction的目的之上还增加了1点:真正地从磁盘上把用户删除 的数据(带墓碑标记的数据)删除掉
-
为什么只有majorCompaction可以真正删除数据
- 因为是真的真的做不到,因为HBase是建立在HDFS这种只有增加删除而没有修改的文件系统之上的, 所以就连用户删除这个动作,在底层都是由新增实现的
-
为什么达到TTL的数据可以被Minor Compaction删除
- 是因为当数据达到TTL的时候,并不需要额外的一个KeyValue来 记录。合并时创建的Scan在查询数据的时候,根据
nowtime - celltimestamp >TTL
来判断cell 是否过期,如果过期了就不返回这条数据。这样当合并完成后,过期的数据因 为没有被写入新文件,自然就消失了
- 是因为当数据达到TTL的时候,并不需要额外的一个KeyValue来 记录。合并时创建的Scan在查询数据的时候,根据
问题参考
-
阻塞急救:出现了服务器数据无法写入、RegionServer频频宕机,参考下面的配置
- RegionServer内存设置得太小,上面有设置方法
- HFile达到允许的最大数量,Memstore就不能刷写数据到HDFS了,之后就是Memstore的阻塞,解决方案调大
hbase.hstore.blockingStoreFiles
- Memstore大小达到阈值:可能是内存设置的太小,或者HFile达到最大值,引起阻塞了,也可以调整
hbase.hregion.memstore.flush.size*hbase.hregion.memstore.block
这两参数 - RegionServer上的Memstore总大小达到阈值:
hbase.regionserver.global.memstore.size
是一个0~1 的数字,代表可占内存的百分比,可以适当调大,但是这个调大,就必须把hfile.block.cache.size
调小,因为两个参数不可以超过0.8
-
RegionServer自杀(朱丽叶暂停):可能发生的原因有
- ZooKeeper在 一个RegionServer太久没有回应的时候会把该节点标记为宕机
- 很大的概率会遇上Full GC,长达数分钟的Full GC很容易造成ZooKeeper将 RegionServer标记为宕机
- 一旦有一台RegionServer被标记为宕机后,平时不怎么干活的 Master就出场了。它开始执行一系列的恢复步骤,包括将集群中 原本属于这台RegionServer的数据移动到其他RegionServer上
- 当这台RegionServer终于从STW从苏醒过来后,再去查看 ZooKeeper发现自己的状 态已经被设置为宕机了。此时 RegionServer显然不能继续像以前一样提供服务了,否则数据就 乱套了,所以这台RegionServer只能自杀
-
解决参考方案
- 考虑内存是否够用,增大内存
- 考虑采取优化GC回收策略的方式
- 除了调整GC回收策略以外还可以启用 MSLAB,MSLAB是MemStore自己管理内存空间的策略,它把堆内存空间分为 一个又一个的Chunk,消除了堆内存中由于回收造成的小碎片,同时开启G1GC效果就更好了
- HBase默认是开启MSLAB的,只是没给它分配chunk而已,效果等同于 关闭,配置方式
hbase.hregion.memstore.mslab.enabled:设置为true即打开 MSLAB,默认为true hbase.hregion.memstore.chunkpool.maxsize:在整个Memstore 可以占用的堆内存中,chunkPool占用的比例。默认值为0.0。把这个值设置成非0.0 的值,才能真正开启MSLAB hbase.hregion.memstore.chunkpool.initialsize:在 RegionServer启动的时候可以预分配 一些空的chunk出来,放到chunkPool里面待使用。该值就代表了预分配的chunk占总的chunkPool的比例。默认值为0.0。如果设置了预分配性能曲线在一开始会更平滑
-
读取性能调优
- 使用过滤器,扫描范围,起始key,结束key之类的
- 增加BlockCache,但是这个还是要看缓存的命中率的,如果命中率较高,那么效果就好,否则就没什么效果,如果能确认了缓存命中较高后,就可以增大
hfile.block.cache.size
的值,但同时要注意BlockCache和MemStore的 总和不能超过0.8,因为至少要留20%的堆内存空间给JVM进行必要的操作 - 调整HFile合并策略:让HFile的数量尽量减 小,以减少每次Scan的跨HFile的次数。但同时又要保证该合并策略适 用于你的场景,并且不要太频繁