HBase-GC性能优化

1 JVM调优

1.1 堆内存

默RegionServer的堆内存为1G,这里Memstore默认站40%,也就是400M,在实际场景中很容易因为Memstore太小导致阻塞,修改参数,在cong/hbase-env.sh:

export HBASE_HEAPSIZE=8G

该参数会将Master和RegionServer的堆内存都设置为8G,所以有需要的话尽量使用专用的堆内存设置项:

export HBASE_MASTER_OPTS="$HBASE_MASTER_OPTS -Xms4g -Xmx4G"
export HBASE_REGIONsERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8G"

就可以将Master调整为4G,RegionServer调整为8G。需要注意的是,不管在什么时候都必须保证物理机留有10%的内存给操作系统做必要的操作。一般来说,如果物理机上还有计算框架,那么RegionServer的内存占有率应该是计算框架之外最大的。
还需要注意的是,如果JDK版本低于8,HBase会有一个内存泄漏的Bug,永久对象区(Permanent Generation,这个区域在非堆内存里边)必须这只如下项:

export HBASE_MASTER_OPTS="$HBASE_MASTER_OPTS -xx:PermSize=128m -XX:MaxPermSize=128m"
export HBASE_REGIONsERVER_OPTS="$HBASE_REGIONSERVER_OPTS -xx:PermSize=128m -XX:MaxPermSize=128m"

如果Xmx调的比较大了,那么需要把PermSize和maxPermSize调整的都大一些(一般不会出现这种情况,128M已经够大了)。JDK8+就不需要这个操作了。

1.2 Full GC的影响

一般来说RegionServer的堆内存越大越好,但是因为垃圾回收的缘故,内存大了之后,相应的FullGC时间也会线性增加,一般来说每G的内存需要的FullGC时间为:。FullGC优势可能达到好几分钟,这个阶段会停止响应任何请求,相当于所有线程挂起,这种暂停又叫做Stop-The-World(STW),FullGc的对HBase造成严重后果比较严重:
在Zookeeper检测RegionServer心跳包的时候,RegionServer正在FullGc无法回应,而如果超过阀值等待时间会被标记为宕机,这时候会将该RegionServer上的数据向其他RegionServer迁移,并且该RegionServerFullGc结束后发现自己被宕机了,为了防止脑裂,会停止自己(RegionServer自杀,又叫朱丽叶暂停)。很多场景下会将zookeeper的心跳检测阀值调大,但是这并不可取。
GC回收策略优化
为了避免长时间FullGC或者减少FullGc的发生,JVM提供了四中GC回收器:

  • 串行回收器:SerialGC
  • 并行回收器:ParallelGC 注重高吞吐量
  • 并发回收器:ConcMarkSweepGC简称CMS 注重低延迟
  • G1GC回收器:Garbage-First GC 吸收了并发和并行回收的特长

1.2.1 ParallelGC和CMS组合方案

并行回收器的性能没有串行回收器好但是FullGC时间较短;而并发回收器主要可以减少老年代的暂停时间,并且可以保证不停止的情况下进行收集,但是每次回留下一些“浮动垃圾”,只能在下次回收的时候被回收。所以Hbase比较符合的配置是:

  • 年轻代使用并行回收器ParallelGC
  • 老年代使用并发回收器ConcMarkSweepGC

依旧在conf/hbase-env.sh,如果需要修改Master的,将RegionServer修改为Master即可:

export HBASE_REGIONsERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xmx8g -Xms8g -XX:+UseParNewGC -XX:+UseConcMarkSweepGC"

1.2.2 G1GC回收方案

G1GC是在JDK1.7.0_O4(jdk7 update4)中新增的,并且如果RegionServer内存很大,大于4G的话就可以考虑G1GC。之所以引入了G1GC,是因为CMS回收依旧不能避免FullGC,发生在如下两种情况:

  • 在CMS工作时候,一些对象经过了多轮幸存,从年轻代移动到了老年代,但是老年代已经空间不足,就回引发STW,暂停,JVM整体又不能响应任何请求了。

  • 当回收回来的内存太细碎,导致新对象放不进去,也只好触发FullGC来整理空间
    G1GC会深情一大块连续的内存空间装在JVMHeap,并被分割成JVM的一个个Region(并不是HBase定义的Region),然后对各个Region单独进行GC,就可以最大限度的避免(不能完全避免)FullGC,并且可以通过手动指定MaxGCPauseMills参数来空值一旦发生FullGC的时候最大的暂停时间,避免时间太长造成的RegionServer自杀:

    export HBASE_REGIONSERVER_OPTS=”HBASE_REGIONSERVER_OPTS -Xms24g -Xms24g -XX:+UseG1GC -XX:MaxGCPauseMillis=100″
    G1GC适合很大的堆内存情况,这里所说的很大指32G、64G以上,如果RegionServer的堆内存设置为4G-32G,需要手动测试才知道G1GC是否适合,手动实验的时候,记得把调试参数加上:

    分别打印GC详细信息、GC时间、survive空间占用日志

    -XX:+PrintGCDetails -XX:PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
    其他调优参数:

    -XX:+UseG1GC #使用G1GC
    -Xms100g -Xmx100g #堆内存范围,这里写死为100G
    -XX:MaxGCPauseMills=100 #G1GC最大的GC时间毫秒
    -XX:ParallelRefProcEnabled #GC使用多线程在Young和mixed GC期间处理增加的引用
    -XX:PaarallelGCThreads=8+(logical processors-8)(5/8) # 多线程回收策略,这里logical processors为40
    -XX:-ResizePLAB #开启线程较多时候,该项可以关闭PLAB的大小调整,避免大量线程通信导致的性能下降
    -XX:G1NewSizePercent=5 #年轻代最小值(占堆内存的比例),默认5%
    -XX:G1NewSizePercent=3 #32GHeap
    -XX:G1NewSizePercent=2 #64GHeap
    -XX:G1NewSizePercent=1 #100G+Heap
    控制年轻代大小的关键是保证其在1G的范围,因为测试发现G1清理Eden空间的速度是每1GB使用100ms
    补充一个HBase的GC测试:针对HBase的Java GC调优

1.3 Memstore的专属JVM策略MSLAB

堆内存非常大的时候,FullGC时间非常长,此时解决FullGC问题不能完全依靠JVM自身的垃圾回收器,MSLAB是为MemStore专门设计的内存管理策略,对标CMS。采用CMS发生FullGC的原因主要有;

  • 同步模式失败(concurrent mode failure):如果在CMS还没有将垃圾回收完成,空间没有完全释放,这个时候新生代的对象过快转化为老年代的对象时候,发现老年代的空间不够了,此时垃圾回收会泛起并发回收转而使用单线程的STW,回到了FullGC。该过程可以通过设置-XX:CMSInitiatingOccupancyFraction=N来缓解,N表示单签JVM启动垃圾回收时候堆内存占用的百分比,设置的越小,垃圾回收越早启动,一般设置为70。
  • 碎片化导致的失败(Promotion Failure due to Fragmentation):当前要从新生代提升到老年代的对象比老年代所有的碎片空间都要大,也会触发STW进行FullGC。这个情况无论怎么调整上述的CMSInitiatingOccupancyFraction都是无法解决的,因为CMS只做回收不会合并,只要RegionServer启动的时间够长一定会导致FullGC。

之所以会出现碎片内存空间,是因为MemStore定期刷写为一个HFile,刷新完成后Memstore所占用的空间就会被回收, 但是因为内存分配都是顺序分配的,导致逐渐出现Memstore位置为碎片空间,产生不连续内存。直到没有任何一块连续内存不能存放新的数据,JVM只好进行STW,使用单进程进行内存空间重新排列。

1.3.1 LTAB方案

不过JVM为了解决碎片内存问题,有一个TLAB(Thread-Local allocation buffer)方案,每个线程都会分配一个固定大小的内存空间,专门给这个线程使用,用完之后释放,新线程也会申请这么大的空间,就不会出现碎片空间。明显的缺点是,如果线程中的对象不需要占用整个线程申请的对象会导致很大的内存浪费闲置,内存空间利用率降低,但是为了避免FullGC还是可以考虑的。
但是HBase不能直接使用该方案,因为HBase的多个Region是被一个线程管理的,多个Memstore占用的空间不能合理分开,于是HBase基于TLAB实现了MSLAB。

1.3.2 MSLAB-Memstore-Local Allocation BUffers

MSLAB完全按照TLAB实现思路,只不过内存是由Memstore来分配的,实现思路为:

  • 引入了chunk的概念,chunk就是一块内存,大小默认为2M
  • RegionServer维护者一个全局的MemStoreChunkPool实例,是一个chunk池
  • 每个MemStore实例中有一个MemStoreLAB实例
  • 当MemStore接收到KeyValue数据的时候先从ChunkPool中申请一个chunk,然后将数据放到chunk中。
  • 如果chunk放满了,新申请一个chunk
  • 如果Memstore因为刷写释放了内存,则按chunk为单位清空内存

堆内存按照chunk为单位划分为规则空间,消除了碎片空间导致的无法插入数据的问题,但是有时候只写入1kB数据也需要2M的chunk空间,但是可以容忍。相关参数如下,在hbase-site.xml中配置:

#设置为true,打开MSLAB,默认为true
hbase.hregion.memstore.mslab.enabled
#每个chunk大小,默认2MB,2048*1024B
hbase.hregion.memstore.mslab.chunksize
#能放入chunk的最大单元格大小,默认为256K
hbase.hregion.memstore.mslab.max.allocation
#在整个memstore可以占用的堆内存中,chunkPool占用的比例,默认为0.0,最大1.0
hbase.hregion.memstore.chunkpool.maxsize
#在RegionServer启动的时候可以预分配一些空的chunk出来放到chunkPool中待使用,改制代表了预分配的chunk占总的chunkPool比例,默认为0.0,最大1.0
hbase.hregion.memstore.chunkpool.initialsize

MSLAB使用注意事项
MSLAB可以和G1GC一起使用,没有冲突。不过看起来MSLAB和G1的实现思路很接近,其实G1是后来才出现的策略,并且根据测试,结合使用的性能会更高。

    原文作者:蠟筆小噺没有烦恼
    原文地址: https://www.jianshu.com/p/605086750c37
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞