优质博文:IT-BLOG-CN
一、背景
业务流量突增,机器直接接入大量流量QPS2000
,JIT
和GC
会消耗太多CPU
资源,导致1-2分钟时间内的请求超时导致异常,因此采用流量预热的方式,让机器逐步接入流量,需要预热时长3min
。目前服务接入HPA
,通过HPA
自动扩缩容应用流量变化,当流量激增时,对机器的启动速度带来了挑战,之前通过Swift
优化点火时间,已经将机器从容器创建到可接入流量优化到2分钟左右,但3min的预热时长成为了应对流量激增的瓶颈,因此优化机器从接入流量到能稳定服务的时长,目标缩减到2min以内。
什么是服务预热: Java
应用在刚启动的时候处理相应速度会很慢,只有当热点代码执行了一定次数以后,相应速度才会达到一个稳定状态。由于Java
慢启动现象的存在,多数情况下我们有必要对Java
应用进行预热,以防止客户端在调用过程中,因为服务器重启或发布事件,而出现大量慢请求。
流量接入后younggc
耗时:峰值900ms
左右,最大次数18次
二、优化思路
名词解释
JIT(Just In Time)
即时编译器: java
程序是解释执行的,即运行时将字节码解释为机器码来执行,因此性能差;为了优化Java
性能,jvm
引入的编译器,随着程序的执行,编译器会将热点代码编译优化为本地代码,来获取更高的执行效率
jvm
中集成了两种编译器:
【1】Client Compiler
:如C1
编译器,注重启动速度和局部的优化,C1
的启动速度开,但是峰值性能比C2
要差;
【2】Server Compiler
:如C2
编译器、Graal
编译器,关注全局的优化,性能会更好,但由于会进行更多的全局分析,所以启动速度会变慢;
【3】分层编译:为了综合Client Complier
和Server Compiler
的特性,在启动速度和峰值性能之间取得平衡,java7
开始引入分层编译,分为5
层:
■ 解释执行。
■ 执行不带profiling
的C1
代码。
■ 执行仅带方法调用次数以及循环回边执行次数profiling
的C1
代码。
■ 执行带所有profiling
的C1
代码。
■ 执行C2
代码。
方法内联: 编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段,JIT
大部分的优化都是在内联的基础上进行的;
逃逸分析: 编译器,根据新建对象是否被存入堆中以及是否传入未知代码(未内联代码)中,判断对象是否逃逸,对未逃逸对象进行锁消除、栈上分配优化;
更多内容参考:JIT & AOP
优化思路
【1】通过调整JVM
参数,提高JIT
效率;
● 增加JIT
线程;
● 调整内联参数,减少内联失败;
● 关闭分层编辑,直接进行C2
编译;
● 关闭逃逸分析,让出资源做其他优化;
【2】更换更新的Server Compiler
:Graal
编译器使用Java
编写,对于Java
而言,尤其是新特性,比如Lambda/Stream
等更优化。
【3】使用AOT
:提前编译,在运行时将Java
方法动态编译为本地AOT
代码,并将它们存储在共享类缓存中,以此提升启动速度,如:DragonWall/openJ9
。
【4】业务代码层优化:减少代码量,针对目前基础策略灰度体检,代码体谅大,灰度结束后,代码量减少,JIT
应当有所好转。
三、优化过程
优化前机器参数:JIT
耗时1.7min
,GC
峰值600ms
调整 JVM参数
【1】采用GraalVM
编译器: 有效果,但效果没有关闭分层编译好。
-XX:+UnlockExperimentalVMOptions
-XX:+UseJVMCICompiler
【2】增加JIT
线程数: 默认15个线程
-XX:+CICompilerCountPerCPU=false
-XX:CICompilerCount=16
【3】增加内联机器码大小阈值,减少内联失败。同时,增加内联调用次数阈值,延迟内联: 无效果,短暂延迟了JIT
耗时峰值;
-XX:+UnlockExperimentalVMOptions
-XX:InlineSmallCode=4000
-XX:InlineFrequencyCount=1000
【4】关闭分层编译: 镜像效果明显
-XX:+UnlockExperimentalVMOptions
-XX:-TieredCompilation
【5】关闭逃逸分析: 效果不明显,有持续耗时高峰,不可用
-XX:+UnlockExperimentalVMOptions
-XX:-DoEscapeAnalysis
AOT
【1】通过openj9
的AOT
替换JIT
: 启动性能要好一些,但是稳定后吞吐量和延迟都要差一点,然后启动时会有部分超过100ms
(大概是首分钟的95线)
【2】使用DragonWall11
: 不支持JWarmup
不可用。
JWarmup
:让JVM
提前知道哪些方法热的,在处理请求之前就让这些方法提前被编译掉,从而避免了前面边解释,边编译的开销。
代码优化
减少代码量: JIT
耗时明显下降。
JVM参数符合使用
【1】采用GraalVM
& 关闭分层编译: JIT
峰值没有改善,且点火时异常增高。最终项目启动成功的成本100288ms
不可用
-XX:+UnlockExperimentalVMOptions
-XX:+UseJVMCICompiler
-XX:-TieredCompilation
【2】采用GraalVM
& 增加内联机器码大小阈值,减少内联失败 & 增加内联调用次数阈值,延迟内联 JTI
峰值没有改善,且点火时长异常高。不可用
-XX:+UnlockExperimentalVMOptions
-XX:+UseJVMCICompiler
-XX:+UnlockDiagnosticVMOptions
-XX:InlineSmallCode=4000
-XX:InlineFrequencyCount=1000
【3】关闭分层编译 & 增加内联机器码大小阈值,减少内联失败 & 增加内联调用次数阈值,延迟内联JTI
峰值没有改善,且GC
耗时过高。不可用
-XX:+UnlockExperimentalVMOptions
-XX:-TieredCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:InlineSmallCode=4000
-XX:InlineFrequencyCount=1000
四、优化结果
【1】JIT-MAX
:JIT
点火耗时Max
,从1.9min
左右,2月1日关闭分层编译后减少到1.6min
左右,代码优化后降到55s
左右
【2】JIT-AVG
:JIT
平均耗时,从原来的10S
,2月1日关闭分层编译后减少到7.5s
左右,代码优化后降到5s
左右
五、结论
【1】分层编译对JIT
耗时有增益效果,但是由于机器差异,对最大耗时的优化不是很明显,从平均耗时看差异较大;
【2】代码重构后,代码量减少,对最大JIT
编译耗时优化效果比较明显,平均耗时也有所下降;
【3】优化QPM
数据采集准确性,减少由于数据采集延迟带来频繁扩缩容,减少JIT
高峰数量;