记一次由RegionServer下线引起的GC调优经历

    今天早上发现在跑的一个Spark任务的失败了,任务内部会涉及到高并发的查询HBase。从Spark任务的界面上来看看,是与HBase的交互出了问题,报错SocketTimeOutException。到服务器上看,发现HBase的RegionServer下线了。查看了下RegionServer的日志,发现报了如下错误org.apache.zookeeper.KeeperException$SessionExpiredException: KeeperErrorCode = Session expired for /hbase/replication/rs,即HBase与Zookeeper之间的会话超时导致了RegionServer的下线。详细的日志如下所示:

 

问题定位:

    定位发现,RegionServer会非常频繁的进行Young GC,并且出现了两次Full GC耗时超过了100s。线上RegionServer的JVM参数配置如下:

    -Xmx32g -Xms16g -Xmn4g -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=85

    (即堆初始大小16G,最大32G,年轻代大小4G。年轻代使用的是ParNewGC垃圾收集器- Serial的并行版本,老年代使用的是CMS垃圾收集器)

 

    从日志里面可以看到, 1s内会就会2~3次Young GC:

 

    随便分析了一条Young GC的日志,年轻代回收了3570817K内存,但仍有374267K的对象进入了老年代。

 

 

一般的JVM调优思路:

  JVM调优原则:

   一般来说JVM调优是最后的手段,最应该做的是优化代码而不是进行JVM调试,减少创建对象的数量,减少大对象。

   但是在现在的这个RegionServer的GC的问题上,代码已经无法修改了,所以直接调整JVM的参数。

  1. Young GC应该尽可能多的收集垃圾,避免让太多对象进到老年代
  2. 尽量减少发生Full GC的频率,Full GC时速度越快越好
  3. 内存越大,吞吐量越好,但是延迟会增加
  4. JVM调优就是在吞吐量、延迟、内存占用三者之间找一个平衡

 

一般的JVM调优思路:

先确定内存

        程序稳定运行一段时间的压力测试之后,建议将Java堆空间设置为3-4倍Full GC后的老年代,新生代占总的堆大小的3/8。

再确定延迟

        确定可接受的平均停顿时间、最大停顿时间、Young GC的频率、Full GC的频率

最后确定吞吐量

        一段时间内可以完成多少个请求或者一个任务在多少时间内执行完毕

 

    对于我的任务来说,肯定是希望越快完成越好,所以是吞吐量优先。但是HBase还是需要响应外部的检索请求的,要求整个检索流程在3s之内响应。

    所以我打算这么调优,既然已经配置好了HBase总的堆内存了,那么内存就先不调整了。先增加年轻代的大小,让对象尽可能少的进入老年代,并且减少Young GC的频率。然后如果还是出现耗时非常长的Full GC的话,再降低内存回收的比例。如果此时延迟还是很大的话,再降低堆内存的大小。

 

 

调优:

提高初始JVM堆内存大小,改为:-Xmx32g -Xms32g -Xmn12g

因为并发量很高,所以JVM初始内存直接设置为最大的32G,目的是为了能够在JVM在GC之后不需要重新计算堆区大小,避免扩展堆。同时增大年轻代内存,目的是为了避免年轻代太小,对象很容易就跑到了

开启任务跑,并使用visualVM进行监控,结果如下所示:

发现Metaspace过了一会就一直都维持在很高的水平,这个会有啥影响么?

    Metaspace这部分空间其实就是JDK8之前的永久代(Perm Gen,HotSpot使用其实现方法区),用于存放类信息、常量、静态变量、即时编译器编译后的代码等数据,所有线程共享。在JDK8之后从JVM中移到了本地内存中,这样几个好处:

  1. Major GC时,不会扫描这个区域,并且简化了CMS处理Permgen的代码
  2. 可以动态增加,之前的Perm是不可变的
  3. 其他和当前情况无关的优化,我也暂时不是很了解

反正知道维持在很高的水平但是能稳定也就无所谓就行了。

 

跑了一会后的堆内存:

 

    可以看到大部分对象都在Young GC阶段被回收掉了,整体堆内存增长的缓慢。

    后来跑了一段时间之后,发现Major GC的耗时超过了3s,影响外部查询响应,所以后来将堆内存从32G调小到了24G,同时将CMS触发Major GC的阈值从85%调整到了75%,实现了3s内响应。

 

 

 

PS:为什么要分堆和栈

    栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

    在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。

 

 

PS:concurrent mode failure

    concurrent mode failure这个错误是在CMS在并行清理垃圾的过程中,因为这个不过程是不会进行STW的,如果此时发现老年代空间不能容纳新产生的垃圾,则会抛出这个异常。然后CMS退化为Serial Old进行垃圾回收,耗时特别久,目前看起来耗时在100s以上!引起这个问题的原因可能有三种:

1.CMS回收触发太晚

2.空间碎片太多

3.垃圾产生的速度太快

 

 

PS:CMS垃圾回收机制概览

CMS往细了说分为6个阶段:

    初始标记(STW initial mark - STW):标记GCRoot能关联到的对象

    并发标记(Concurrent marking):GCRoot继续向下标记

    并发预清理(Concurrent precleaning):查询在上个阶段进入老年代的对象,减少下个阶段的扫描,因为下个阶段要STW

    重新标记(STW remark - STW):从GCRoot向下追溯,确定要回收的对象

    并发清理(Concurrent sweeping):清理垃圾

    并发重置(Concurrent reset):重置CMS的数据结构,等待下一次垃圾回收

 

 

 

 

 

 

参考:

    https://blog.csdn.net/yangguosb/article/details/79857844(concurrent mode failure原因)

    https://gceasy.io/index.jsp#banner(gceasy网站)

    https://blog.csdn.net/yangzhengjianglove/article/details/81233117(为什么要分堆和栈)

    https://segmentfault.com/a/1190000018191673(阿里大佬总结的JVM调优原则)

    https://blog.csdn.net/wfh6732/article/details/57490195(CMS的垃圾回收过程)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值