天天看点

优酷土豆资深工程师:GC 调优实战

对于线上高并发、高吞吐的java web服务来说,长时间的gc暂停(也叫 stop- the-world)会严重影响系统吞吐、稳定性和用户体验。下文是我们的一个真实线上web系统针对gc调优过程的一个总结。这个系统在调优前,经常会反映有超秒的gc暂停问题,这种gc问题可能会导致调用方(可能是上层服务调用方、负载均衡层或客户端)阻塞、超时、甚至雪崩的情况。我们在系统资源不变的情况下,经过多轮调优,大幅降低了gc的频率和暂停时间。

1、统计应用数据(峰值tps、平均tps,每秒平均分配内存大小、每个请求的平均分配内存大小)

2、统计gc分配、回收内存的数据(minorgc、fullgc停顿时长,平均多长时间触发一次gc,每次eden->old的平均晋升大小等)

3、搭建压力测试环境

4、模拟线上真实用户行为及相应压力(记录用户访问的accesslog作为压力测试源,使用的压力测试软件为http_load和httperf)

2、改为使用jdk6 update26版本的g1回收。设置最大回收时间为40ms,通过12小时的观察,发现有大量超时,感觉g1在jdk6上还不够成熟,所以决定暂时放弃g1,改为parallelgc;

3、使用parallelgc后,压力测试发现每次minorgc的耗时降低到40ms左右(以前是200ms以上),但每隔3小时就会有一次fullgc发生,每次fullgc耗时3~4秒;

4、由于fullgc造成的应用暂停在这个应用中是不能接受的。所以放弃parallelgc,改为使用cmsgc。

1、观察 gcutil 发现permspace接近100%,调大permsize 和 maxpermsize;

2、调整-xms和-xmx相等(如果xms小于xmx,则应用启动初期老生代相对较小,会导致cms gc更加频繁);

3、尝试优化每次parnew的时长(优化前每次在200ms以上):

增加“-xx:+printtenuringdistribution”参数观察gc.log,发现对象在survivorspace中的age过多,会导致大量老对象在新生代无法晋升到老生代。而jvm在parnewgc时分析这些老对象的引用关系是非常耗时的。观察maxtenuringthresh-old 和 targetsurvivorratio 设置的过大,所以将 maxtenuringthreshold 值调小为15即达到优化目的(优化后每次parnew在20~40ms之间)。并且为了提高survivorspace的利用率,将targetsurvivorratio设置为100(代表强制gc关闭动态调整maxtenuringthreshold,这个参数设置为100会略为激进,增加了survivor区使用率的同时,降低了survivor区应对突发流量的承载能力,不同应用可以看情况调整)。

4、尝试优化parnew之间的间隔时间(优化前3~4秒一次):观察gc.log发现每次parnew后大约有不到780mb的存留对象,希望这些对象尽量活在survivorspace里,并且同时又要保证parnew的时间间隔,所以在xmx和survivorratio不变的情况下,将xmn扩大到7800mb。(因为survivorratio=8,所以整个edenspace需要780*10=7800mb)

5、再次观察优化后的gc情况(gcutil),发现由于大量对象都在edenspace消亡,所以oldgen的晋升比率极低(0.01%~0.02%),所以可以考虑增大cmsinitiatingoccupancyfraction以提高oldgen的利用率,降低cms gc的触发频率(增大到80%)。

6、去掉cmsfullgcsbeforecompaction(去掉后默认为0,表示每次fullgc后都会进行压缩碎片整理)。因为cms gc导致的内存碎片必须清除,否则oldgen的利用率会降低。

运营一段时间后,发现cmsgc超过一秒的情况非常多(图中箭头指向):

优酷土豆资深工程师:GC 调优实战

gc日志:

优酷土豆资深工程师:GC 调优实战

可以看出,在remark中的rescan阶段耗费了1.57秒,并且这个过程是会导致应用暂停的。问题定位在了rescan阶段。

发现在rescan时新生代过大(4313641 k(7188480 k)),是导致rescan慢的关键原因,如果能尽量保持新生代很小的时候就终止preclean阶段,就可以控制住在rescan时新生代的大小。查看jvm参数发现-xx:cmsscheduleremarkedenpenetration的意思是当新生代存活对象占edenspace的比例超过多少时,终止preclean阶段并进入remark阶段。这个参数的默认值是50%,按照现在的配置,就是7800m*50%=3900m左右,所以更改此参数设置为: -xx:cmsscheduleremarkedenpenetration=1

进行压力测试,发现remark阶段的耗时确实降低了不少,说明优化有效。

运行几天后观察gc日志(2011-09-05),发现每隔100000秒的cmsgc的峰值情况确实大大降低了,但是还是偶尔有超过1~2秒的cmsgc情况:

优酷土豆资深工程师:GC 调优实战
优酷土豆资深工程师:GC 调优实战

发现concurrent-abortable-preclean阶段超过了-xx:cmsmaxabortableprecleantime 设置的最大值10秒,所以强制终止了preclean阶段而进入remark阶段。而这段时间的两次parnew之间的间隔了17秒之多。希望的是在preclean阶段产生一次minorgc,所以将preclean的最大时长调整为30秒: -xx:cmsmaxabortableprecleantime=30000

运行一段时间后,发现居然出现了fullgc,大概在3~5天左右出现一次,以下是fullgc时的日志:

优酷土豆资深工程师:GC 调优实战

发现在443310秒有promotion failed出现(新生代晋升到老生代空间不足导致的fullgc),但是此时的oldgen可以算出还剩1.45g的空间(5324800k-3871691k=1453109k),而根据gclogviewer的统计,每次minorgc后平均新生代晋升到老生代的内存大小仅为58k。所以并不是oldgen空间不够,而是oldgen的连续空间不够造成的promotion failed。

换句话说,是由于oldgen在距离上次cmsgc后,又产生了大量内存碎片,当某个时间点在oldgen中的连续空间没有一块足够58k的话,就会导致的promotion failed。以下是sun针对这个问题的说明:

考虑如果能够缩短cmsgc的周期,保证在出现promotion failed之前就进行cmsgc,就可以避免这个问题了。所以考虑将新生代空间缩小(相对来说就增加了老生代的空间),并且将cmsgc触发比率降低,同时保证survivor空间不变。所以优化参数改动如下:

上面的调优保持系统稳定运行了很长时间后,突然有一台机器出现大量fullgc,观察gc.log发现是由于持久带满造成的:

优酷土豆资深工程师:GC 调优实战

应对的方法为加大持久带,并让持久带也使用cmsgc方式回收:

精细化的gc调优是需要耐心和时间的,往往一轮调优要经过gc数据集、分析、调整参数、压力测试、灰度发布、最终上线这几步,上线一段时间后,通过监控发现有新的gc问题,可能又会需要再一轮的调优。而且系统版本的迭代、对象生命周期的变化、线上流量和服务依赖的变化,都可能会对gc频率和时间有影响。所以对于线上的重点项目,建议每次大版本上线前都能建立一个gc监控、收集和调优的意识,最大程度上规避gc对系统带来的风险。

<b>优化后整体参数</b>

<b>作者简介:</b>高嵩,优酷土豆大数据基础平台资深工程师,热衷于高并发、高可用、分布式领域。热爱开源,为人和善,乐于分享。

<b>  </b><b>                                                  中生代技术分享群微信公众号</b>

优酷土豆资深工程师:GC 调优实战