之前的文章分别讲了优雅上线 和 优雅下线,实际工作中做了优雅上下线后,服务发布后还是会有短暂的“抖动”,接口的响应时间急剧升高后又恢复正常,就和下面的监控图一样,图片来源于 得物 的InfoQ技术文档服务发布时网络“抖动”
背景
小卷现在负责的系统已经达到20万QPS了,每天即使是在半夜,QPS依然过万。每次系统升级发布时,抖动比较频繁,上游应用方都跑过来质问,怎么服务又超时了啊,还能不能用了。。。(巴拉巴拉),小卷只能陪着笑脸的一番解释。后来小卷加上了优雅上下线,想着这下发布应该没问题了吧。哪知再次发布,超时问题依然存在。。。小卷决定好好分析一下发布抖动问题的根因是啥
1.抖动问题分析
服务抖动问题需要根据具体场景分析,这里列一下可能的原因:
- redis、DB连接初始化耗时长,引起启动后的接口RT升高
- JIT即时编译耗时长,造成CPU利用率高,引起接口RT升高
对于高并发的应用来说,这里JIT即时编译是通用的原因。
JIT是什么?
JIT(just-in-time)即时编译,是一种执行计算机代码的方法,这种方法涉及在程序执行过程中(在执行期)而不是在执行之前进行。关于JIT的历史,摘抄一段*上的内容
最早发布的JIT编译器是 约翰·麦卡锡在1960年对LISP的研究。在他的重要论文《符号表达式的递归函数及其在机器上的计算》(Recursive functions of symbolic expressions and their computation by machine, Part I)提到了在运行时被转换的函数,因此不需要保存编译器输出来打孔卡。在Self被Sun公司抛弃后,研究转向了Java语言。“即时编译”这个术语是从制造术语“及时”中借来的,并由Java普及,Java之父James Gosling从1993年开始使用这个术语。目前,大多数Java虚拟机的实现都使用JIT技术,而且使用广泛。
了解JVM的都知道,Java的编译分为两部分:
-
javac
将.java
文件编译为.class
文件,即转换为字节码 - 解释器将
.class
字节码文件解释为机器码(0、1)执行
但是解释执行的缺点很明显,执行速度慢。
Java早期使用解释执行,将字节码逐条解释执行,这种方式运行很慢。如果是快速反复调用某段代码,执行效率大大降低。后来为了解决这种问题,JVM引入了JIT即时编译,当Java虚拟机发现某段代码块或是方法执行比较频繁,超过设定的阈值时,就会把这些代码视为热点代码(Hot Spot code)
为了提高热点代码的执行效率,虚拟机会将其编译为机器码,并存到CodeCache里,等到下次再执行这段代码时,直接从CodeCache里取,直接执行,大大提升了运行效率,整个执行过程如下:
看上图很容易理解JIT是什么,然后思考下面的问题:
- 怎么判断属于热点代码?
- 阈值是怎么设定的?
- codeCache又是什么?
怎么判断热点代码
我们知道JIT是将热点代码编译成机器码缓存起来的,那么什么样的代码才属于热点代码呢
HotSpot虚拟机使用的是基于计数器的热点代码探测,JVM统计每个方法调用栈的弹出频率作为指标,提供了2种次数级别热点探测方法:
- 精确计数,超过阈值触发编译 (统计的是总调用量)
- 记录一段时间内被调用的次数,超过阈值触发编译(类似QPS的含义)
JVM默认使用的第二种方法统计方法调用次数,因为第一种方法计算开销大,第二种方法与调用时间有关,适用于大多数场景
阈值如何设定
上面说到超过阈值才触发编译,阈值是设置为多少了呢?
先说说JVM的分层编译器,Hotspot虚拟机中,JIT有2种编译器C1编译器(客户端模式)、C2编译器(服务端模式)。
C1编译器:简单快速,搜集信息较少,主要关注点在局部化的优化,编译速度快,适用于对启动性能有要求的应用。缺点是编译后的代码执行效率低;
C2编译器:需要搜集大量的统计信息在编译时进行优化,为长期运行的应用程序做性能优化的编译器,优化手段复杂,编译时间长,编译出来的机器码执行效率高。代价是启动时间变长,程序需要执行较长时间后,才能达到最佳性能;
JAVA8之后默认开启了分层编译,即:应用启动初期使用C1编译器缓存热点代码,在系统稳定后使用C2编译器继续优化性能。
可通过一些参数进行设置
在 Java8 中默认开启分层编译(-XX:+TieredCompilation默认为true)
- 如果只想用 C1,可以在打开分层编译的同时使用参数“-XX:TieredStopAtLevel=1”
- 如果只想用 C2,使用参数“-XX:-TieredCompilation=false”关闭分层编译即可
通过java -version
可看到当前JVM使用的编译模式
方法被调用的次数,在 C1 模式下默认阈值是 1500 次,在 C2 模式是 10000 次,可通过参数-XX: CompileThreshold
手动设定,在分层编译的情况下,-XX: CompileThreshold
指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。超过阈值触发编译,编译完成后系统会把方法调用入口改为最新地址,下次直接使用机器码。
需要注意的是,计数器统计的是一段时间内的调用次数,当超过时间限度调用次数仍然未达到阈值,那么该方法的调用次数就会减半,并不是一直累加的,这段时间称为该方法的统计半衰周期,可以使用虚拟机参数-XX:-UseCounterDecay
关闭热度衰减,参数-XX:CounterHalfLifeTime
设置半衰周期的时间,需要注意进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的。
CodeCache是什么
CodeCache主要用于存储JIT编译后的机器码,随着程序的运行,大部分热点代码都会编译为机器码来运行。所以Java的运行速度比较快,除了JIT编译的代码外,本地方法代码(JNI)也会存储在Codecache内。可配置一些参数设置Codecache的属性
- -XX:ReservedCodeCacheSize:codeCache最大大小
- -XX:InitialCodeCacheSize:codeCache初始大小
在Linux环境下,Codecache默认大小是2.4375M,可通过jinfo -flag InitialCodeCacheSize [java进程ID]
查看,如图
2.为什么应用刚启动时会抖动?
上面已经讲了JIT即时编译,这样也好理解为什么刚启动完的应用,RT突然升高,CPU利用率也很高。在高并发场景下,一个方法的调用次数激增,会瞬间达到JIT编译的阈值,JVM会执行即时编译,讲热点代码转为机器码。热点代码过多时,JIT编译的压力会增大,造成系统的load升高,CPU利用率跟着升高,导致服务的整体性能下降
3.解决方案
这里小卷列了一些解决方案,需要根据具体场景具体使用,如图
JWarmup
AJDK内嵌的功能模块,相关wiki在阿里的Github上阿里巴巴Dragonwell8用户指南
其原理是先发布beta服务器,等到beta服务器的JIT编译完成后,将热点方法dump下来,然后production环境发布时直接加载dump文件,不需要再进行JIT编译了。从JVM层面解决了该问题,但是接入门槛较高,可能会踩一些坑。
平台预热
借助流量调度平台的能力,小流量预热后再放开,把JIT编译的影响降低。是综合考虑接入成本以及推广维护最合适的方案。这里阿里云微服务引擎MSE已提供功能小流量预热服务,但是是收费的哦~
关注我
我是卷福同学
,公众号同名,在福报厂修福报的小卷哦~