我所负责的系统有一个每天更新报表的离线任务。每天处理大概1千万的数据。任务的基本情况是单个节点执行,总时间大概在4.5小时左右,fullgc非常频繁,甚至会造成用户线程长时间停顿。原因也很简单,线程数比较多,每个线程处理的数据比较多,minor gc之后存活的对象大于S0的75%,导致全部进入老年代,并且新对象产生的速度远远低于gc的速度。导致基本1秒一次full gc。
一期优化我们的离线任务使用的是xxl job调度。由4个节点构成的集群。所以可以使用分片的方式分到4个节点去执行。这样每个节点处理300万数据。执行时间会降低到1小时左右。这样是的数据库主库的cpu使用率升高了原来的4倍,到40%左右。一期优化思路比较简单,虽然执行时间降到了一个小时。但是gc的问题还是没有得到解决。full gc依旧很高。
二期优化(失败)因为我这个Q的OKR有一个任务是将这个任务的执行时间降到半小时内。为了提供一个故障的快速恢复能力。所以我认真分析了下执行的基本情况。存在如下几个问题
1.查询这300万数据是同过limit 2000的方式查询,这样每次在查询的时候都需要过滤到前n*2000条记录,到后边会越来越慢。
1.处理的数据量太多。20个线程,每个线程处理2万左右数据。每一条记录大概1kb。所以理论上同一时刻内存中有20*20000*1kb = 400mb,我们的机器配置是12核,24个超级线程,128g。jvm配置是6g,年轻代2g,老年代4g,S0,S1大概是2g*10% = 200mb左右。晋升到老年待的默认配置是50%,也就是只要大于100mb的存活对象就全部晋升到老年代了。所以很明显400mb的对象每次都进入老年代。老年代很快就到75%,然后开始频繁的gc。频繁的暂停用户线程。
2.处理完的数据要更新回数据库,目前是通过一条一条update,这样会有大量的网络io。
问题处理针对以上问题
1.处理方式改为主键查询,id > num limit 2000,这样的方式mysql会通过注解id快速定位到要查询的第一条记录。然后快速向后查询满足条件的2000条记录然后返回
2.首先将S区的晋升比例调整为80%,这样理论上内存中可以存在160mb的数据。然后按照160mb的数据开始调整线程数和数据量。因为任务只要是一个io任务,所有线程数调整为24*3 = 72,数据量调整为2500,这样10*16000*1kb = 160mb,理论上可以保证在一次gc之后不会晋升到老年代。
3.单条sql的更新改为500的批处理。减少网络io,通过计算,批处理的方式比原来快7倍
优化结果对象情况,和业务有关的对象只有3和9.
gc情况
优化之后执行时间增加了20分钟,7000次的minor gc 有2560多次full gc,可以看出这是一次失败的优化。
事后我分析是因为我在计算内存可用量的时候太理想化了,160mb的数据不可能全是我的业务数据,比如截图1中还有好多其他类型的对象所以才会有有这个结果,导致后续流程中批量操作的优势都没有体现出来,反而增加了10分钟,因为频繁的gc导致服务基本不可用
三期优化线程数不变,还是72,把数据量降低一半到1500左右
优化结果优化之后执行时间为25分钟,minor gc1000次。full gc10次,机器cpu到400%,数数据库QPS到1万,cpu idle降到40%。



