CPU异常排查和JVM性能调优

背景


项目中,经常会发现系统运行一段时间后会很慢,经简单排查发现系统的CPU满负荷运行。以下是总结排查过程


服务器问题排查


线程问题排查


在服务器上查看cpu异常的具体线程,通过打印线程栈信息确定问题发生点,以此来排查问题。
参考文献:https://blog.csdn.net/gufenchen/article/details/99877714


具体步骤如下:

CPU异常排查和JVM性能调优_第1张图片
找其它几个线程,使用同样的方法发现,这些线程主要是GC线程。


发现系统频繁的进行Full GC,并且每次Full GC后,永久代一直是满的,说明永久代中的数据被GC,Roots关联着,通常原因是开发人员使用静态集合对象持有了数据的引用,所以不会清除。


代码问题排查


根据线程问题定位的结果,排查此次更新的代码中是否有静态集合,经过排查发现,更新的代码没有存,在这种情况。同时代码再往上追溯一个版本,发现所有的更新代码也没有这种情况。


日志排查问题


查看的系统日志,发现请求2020协议时,有时会出现栈溢出错误,并且打印出大量的重复日志。某个协议存在问题,陷入死循环


# 查看进程信息
top
# 找到CPU占用最高的进程,这里肯定就是我们的TOMCAT启动的JAVA进程,假设PID为6415
top -p 6415
# 进入进程后,查看进程中哪个线程CPU使用率比较高
H
# 假设CPU使用率最高的线程为7065,转成16进制结果为:1b99
printf "%x\n" 7065
# 查看线程所在行的后10行,确定问题发生原因
jstack 6415 | grep -A 10 1b99
# 查看堆空间变化,6415为PID,每2秒打印一次,打印2000次进程GC信息
jstat -gc 6415 2000 2000

经过分析尝试修改2020协议异常处理验证是否解决问题,同时因为永久代无法回收,尝试提高JVM的内
存大小,看是否能解决问题。在TOMCAT的启动文件catalina.sh中添加启动参数:
上面的问题在重启TOMCAT时,会有信息打印出来,因为如果JVM内存设置过大也会提醒这个信息,所
以选择性忽略,如果去排查下参数,或许可以避免这个问题。
重启服务器后,观察日志输出,发现开始会出现栈溢出的日志,经过一段时间后,没有发现栈溢出的日
志。
使用 jstat -gc 6415 2000 2000 查看堆空间变化,依旧发现频繁的Full GC。(其实这里已经基本
解决问题了,这个后续再说)
为了确认问题,还查看了系统的存活对象列表,同时dump了一份JVM内存快照,便于后续分析。
分析内存快照
内存的分析文件有3G多,下载期间使用阿里的arthas进行故障排查。主要目的是想查找占主要内存的是
具体对象是什么,便于进一步确认问题。但是arthas无法达到这个目的。arthas这个软件很强大,比如
第一章节的线程分析,使用它的话会非常方便。
使用MAT分析JVM内存文件,需要使用Eclipse安装MAT插件,因为JVM内存文件较大,需要配置Eclipse
的JVM内存大小才可以。
分析结果如下:
# 这里我犯了一个严重的错误,原计划是要设置堆内存最小内存是4G,应该设置为-Xms4096m,这里设置
成-Xmn4096m,代表年轻代空间为4G,因为总内存为4G,因此导致永久代的空间为0.
-XX:MetaspaceSize=521M -XX:MaxMetaspaceSize=512M 这两个参数代表元空间大小,元空间是
JDK1.8后的概念,主要存放系统的常量、静态变量、类信息等。使用的是系统的直接内存,因为初始值为
21M,太小会导致系统进行Full GC,同时会进行动态扩容或收缩。
JAVA_OPTS='-server -Xmx4096m -Xmn4096m -XX:MetaspaceSize=521M
-XX:MaxMetaspaceSize=512M'
# 查看系统的对象大小列表,是否有大对象存在,排查发现占主要内存大小的是short[]和int[],不知
道具体什么对象,跟应用相关的对象占空间很小。
jmap -histo:live 6415 > live.txt
# dump内存信息
jmap -dump:format=b,file=heap.hprof 6415

从上图可以看到系统主要内存被三部分内容占用,点开druid的那个,查看占用空间最大的项为学时上
报的2020的批量插入sql,sql需要插入17000多条数据。结合资料确定因为开启了druid的SQL统计功
能,因此导致内部Map持有每一条SQL的引用,因为SQL使用Mybatis的foreach进行拼接,每次的SQL
都不一样,所以占据大量空间。druid在分析SQL时使用递归的方式解析时会导致栈溢出,这就是导致此
次事故的主要原因。
参考:https://segmentfault.com/a/1190000021636834?utm_source=tag-newest
各种资料的建议是关闭系统的druid的SQL统计。因此修改配置重新启动应用,观察是否有问题。
JVM参数调优
禁用了SQL统计后,同时进行JVM参数优化时,发现了自己疏忽导致参数配置错误的问题。新的配置如
下:
对一些新的参数配置的说明如下:
1. -Xmx3072m -Xms3072m 堆内存只分配了3G,因为确定了故障原因后,不是堆大小的原因引起,
因此减小内存,留出更多内存给系统运行。
2. -Xmn2048m 默认年轻代占堆内存的1/3,我们的系统永久代中主要就存放些Spring相关的对象,
偶尔大的对象会直接分配到永久代,因此给年轻代大点空间,可以减少YGC的频率,提高整体效
率。永久代1G的空间也足够使用了。
3. -XX:+UseConcMarkSweepGC 使用CMS垃圾回收算法,可以更高效率的回收系统垃圾,CMS主要
是回收永久代的垃圾算法,降低Full GC的STW(Stop The World)的时间。
4. -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m 相比开始降低了元空间的大小,使
用arthas分析时发现元空间的大小基本在130M左右,因此256M的空间足够了。
5. -XX:MaxTenuringThreshold=5 YGC时会在S0和S1区一直拷⻉数据,默认是拷⻉15次,才会移
动到永久代,设置为5次,是降低系统开始启动时,大量需要进入永久代的对象一直在S0和S1进行
拷⻉。
6. 其它配置时输出GC时的日志信息,观察GC的信息。进行系统调优。
重新启动后,发现系统的CPU恢复正常。
使用 jstat -gc 6415 2000 2000 查看堆空间变化,发现系统还是会偶尔随机的连续进行两次Full
GC,查看GC日志文件,发现引起Full GC的全部是 System.gc() 。通过代码排查发现, LocalCache
类中调用了 System.gc() ,但是业务逻辑中没有使用调用此类的接口,其它调用GC的地方最可能是jxl
中进行excel处理时会调用 System.gc() 。
因此增加JVM参数 -XX:+DisableExplicitGC ,屏蔽 System.gc() 的调用。
修改参数再次重启。系统GC正常,一般维持系统几分钟一次YGC,几个小时一次Full GC即可。参数的
调整需要根据业务的实际使用情况进行调整。
JAVA_OPTS='-server -Xmx3072m -Xms3072m -Xmn2048m -XX:+UseConcMarkSweepGC -
XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:MaxTenuringThreshold=5 -
Xloggc:/App/yunxuexi/apache-tomcat-8.5.31/logs/gc-%t.log
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -
XX:+PrintGCCause -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -
XX:GCLogFileSize=100M -XX:+DisableExplicitGC'

总结


此次故障的排查,单从解决问题的⻆度上,走了很多的弯路,尤其是配置错误的JVM参数后,导致了系统频繁的Full GC,不是因为SQL统计的问题导致的,另一方面却让我对此次故障深层次的原因有了全面的了解。


在项目的开发中需要注意:


1. 涉及到批量操作的地方,要进行数量限制。
2. 其它项目涉及2020接口的地方,需要做异常处理,返回正确结果,宁愿少一次数据,不能让后续
都失败。
3. 在对系统参数修改时,需要备份和最后的检查,确保不会引入新的问题。
4. 在排查代码中发现加锁时未处理异常时进行锁释放。锁的key设计不合理,学时上报把上报内容作
为锁key,导致redis存储超大key的问题。
5. 操作文件流时未进行资源的释放。调用ffmpeg处理视频时,未处理流关闭。

你可能感兴趣的:(jvm)