当前位置: 首页 > 后端技术 > Java

【故障排除系列】C2编译器线程引起的CPU抖动

时间:2023-04-02 10:24:50 Java

1.前言本文仅分享本人在工作中遇到的问题的解决方法和思路,以及排查过程。重点是分享调查的思路,知识点其实挺老的。如果您有任何疑问或描述不当,请告诉我。2.问题是在项目启动的时候,系统请求会有一波超时。从监控来看,JVM的GC(G1)波动较大,CPU波动较大,各业务使用的线程池波动较大,外部IO耗时增加。3、先说结论。由于JIT的优化,在系统启动时会触发热点代码的编译,针对C2进行编译,导致CPU占用率过高,进而引发一系列问题,最终导致部分请求超时。4.在调查过程中,知识点其实是放在那里的。如何把遇到的实际问题和知识点联系起来,更深入地理解这部分知识,这可能就是调查分析的意义,然后沉淀成经验,然后成长起来。4.1初探其实我们的项目是一个算法排序的项目,里面或多或少的加入了一些小模型和大大小小的缓存,而且从监控的角度来看,JVM的GCspurt和CPUspurttime很接近(这也是一个监控平台时间不够准确的原因)。所以前期花了大量的精力和时间排查JVM和GC的问题。首先给大家推荐一个网站:https://gceasy.io/,这个网站对于分析GC日志真的很有用。使用以下JVM参数打印GC日志:-XX:+PrintGC输出GC日志-XX:+PrintGCDetails输出GC详细日志-XX:+PrintGCTimeStamps输出GC时间戳(以基准时间的形式,相当于12:00,什么都没有做实时)-XX:+PrintGCDateStamps输出GC时间戳(日期格式,如2013-05-04T21:53:59.234+0800)-Xloggc:../logs/gc.log日志文件的输出路径YGC很严重,所以我尝试了以下方法:调整JVM的堆大小。即-Xms、-Xmx参数。无效的。调整回收线程数。即-XX:ConcGCThreads参数。无效的。调整预期单次恢复时间。也就是说,-XX:MaxGCPauseMillis参数无效,甚至更糟。以上调整和混合测试均无效。鸡贼的方法。加载完模型后,sleep一段时间,让GC稳定下来,再把请求放进去。这样操作后,GC确实好转了,但是最初的请求还是有超时的。(当然,因为问题根本不在GC上)4.2另一种思路根据监控,线程池、外部IO、RT在启动时都有明显的RT先升后降,而且趋势非常持续的。这些一般都是系统问题导致的,比如CPU、GC、网卡、云主机超售、机房延迟等等。那么既然GC治不了,那我们就从CPU入手吧。因为JVM在系统启动的时候会产生大量的GC,无法区分是因为流量来之前系统还没有预热,还是无论启动多久,一来就会出问题随着交通的到来。而我之前查看了GC运行,也就是加了sleep时间,恰好帮我看到了这个问题,因为可以很明显的看到GC波动的时间和超时的时间在时间??上相差很多,也就是say,跟GC没有关系,GC再顺畅,流量来了还是会超时。4.3分析工具Arthas不得不说,Arthas真的是一个非常好用的分析工具,省去了很多复杂的操作。Arthas文档:https://arthas.aliyun.com/doc...其实分析的核心是流量刚来的时候我们的CPU做了什么,所以我们用Arthas来分析流量来的时候CPU的情况。其实这部分也可以用top-Hppid、jstack等命令完成,不再展开描述。对于CPU情况,只展示了重要的部分:从图中可以看出,C2CompilerThread占用了很多CPU资源。4.4问题核心那么这个C2CompilerThread到底是什么。《深入理解JAVA虚拟机》其实这部分是有说明的,所以这里我就用通俗易懂的语言给大家讲解一下。其实Java一开始运行的时候,你可以理解为傻傻的执行你写的代码,这叫“解释器”。这样做的好处是速度会很快,Java很快就变成.class了。可以启动运行,但是问题也很明显,就是运行慢,所以聪明的JVM开发者做了一件事,如果他们发现你有一些代码是经常执行的,那么他们就会在运行期间,我会帮你把这段代码编译成机器码,这样运行起来会很快。这就是即时编译(just-in-timecompilation,简称JIT)。但是这样也有一个问题,就是编译的时候很耗CPU。而C2CompilerThread只是JIT中的一层优化(一共五层,C2是第五层)。所以,罪魁祸首已经找到了。5.尝试解决解释器和编译器之间的关系如下:上面说了,解释器启动很快,但是执行起来很慢。编译器分为以下五个级别。第0层:程序解释执行,默认开启性能监控功能(Profiling)。如果不启用,可以触发第二层编译;Layer1:可称为C1编译,将字节码编译成本地代码,实现简单、可靠的优化,不启用profiling;layer2:也称为C1编译,启用profiling,只进行C1编译,profiling方法调用次数和loopback执行次数;layer3:也称为C1编译,用Profiling进行所有的C1编译;Layer4:可以称为C2编译,也是将bytecode编译成nativecode,但是会启用一些编译时间比较长的优化,甚至会根据性能监控信息进行一些不靠谱的攻击性优化。所以我们可以尝试从C1和C2编译器的角度来解决问题。5.1添加参数关闭分层编译:-XX:-TieredCompilation-client(关闭分层编译,开启C1编译)效果较差。CPU使用率持续偏高(与调整前相比)。确实没有C2线程的问题,但是猜测是因为代码编译不如C2,导致代码持续性能低下。CPU截图:5.2增加C2线程数增加参数:-XX:CICompilerCount=8恢复参数:-XX:+TieredCompilation效果一般,还有请求超时。但是会少一些。CPU截图:5.3Inference其实从上面的分析可以看出,如果不能绕过C2,肯定是有些抖动的。如果绕过C2,整体性能会低很多。这是我们不想看到的,所以关闭C1,C2,我没试过直接用解释器模式运行。5.4最终解决方案由于这部分抖动是绕不过去的,所以我们可以使用一些模拟流量来承受这部分抖动,也可以称之为warm-up。在项目启动时,使用预先录制的流量完成系统的热点代码Just-in-time编译,然后接收真实流量,这样就可以达到真实流量无抖动的效果。下面这篇文章重点分享解决和分析的过程,知识点不着重分析。更多知识点请查看“参考文章”部分。本文如有问题,欢迎指正。参考文章【关于java:-XX:--TieredCompilation到底是做什么的】https://www.codenong.com/3872...【好像是上面文章的原文】https://stackoverflow。com/que...【C2CompilerThread】https://blog.csdn.net/chenxiu...【C2CompilerThread9长时间CPU占用解决方案】https://blog.csdn.net/m0_3788...《深入理解Java虚拟机第二版》第四篇《Late(Runtime)Optimization》【深度剖析JVM中线程的创建和运行原理||JIT(未来)】https://www.cnblogs.com/silyv...【分层编译HotSpot虚拟机(分层编译)】https://blog.csdn.net/u013490...