神秘的偶发服务超时,原因可能是那些坏邻居

1. 恶邻A君

唯品会在服务化体系改造的初期,一个对延时敏感的应用,偶然会发生一些超时,事发当时zabbix分钟级监控,dstat秒级监控的服务器指标都正常,应用,数据库,缓存,网络也正常,那这是为什么呢?

某天脑洞大开,把怀疑的目光投向了在后台运行日志收集程序Flume,发现它的GC运行得比较狂野,于是对它的GC线程数做了限制:

修改前:15分钟内, 大于30ms的业务调用173次, 大于50ms的23次

修改后: 246分钟内,大于30ms的业务调用41次, 大于50ms的4次

2. 恶邻B君

又过了若干个月,又有某些应用,又开始抽风。这次相对好查一些,因为我们新升级了服务器的监控系统,只要在两台机器上做一下对比测试就好了。 只花了一个晚上,基本就能验明凶手了。

那这个新升级的监控系统,又是怎么影响到主应用的呢?找出它与应用有交互的部分,原来对于JVM的各种线程数信息,堆内存各代的信息,每拿一个数据都会启动一次JMX Client,所以每分钟都有一秒要连拿7个数据,启动7个JMX Client。

改进方法很简单,我们自己定制了一下JMX Client,将7个数据合并在一个命令里获得,另外定制了一下JMX Client的JVM参数,将它启动的动静尽量减少。

3. 逆优化

可见,JVM是个运行服务端应用的好VM,但体量有点大。如果你只是想频繁地运行一段Java写的脚本,或者在跑一些辅助性的程序比如监控和日志收集,往常推荐的JVM参数也就不再合适里,需要进行逆优化才能做个安静的好邻居:

一、启动快速,动静小。

二、低成本,节约CPU、内存和线程。

三、低扰动,不干扰主应用的运行。

4. 从失败的取经开始

第一时间,觉得和JDK自带的jmap,jstack们用一样的参数就好了,多简单。

在它们运行时,跑jps -v,结果发现通通只有一个-Xms8m 。

还不死心,又去翻源码,JDK7在Makefile.launcher,JDK8在CompileLaunchers.gmk,结果发现全部8M,通通8M,再没别的参数了。

有同学又从久远的记忆中想起一个-client,感觉也是比较弱气的选项,但在这个多核的64位Linux服务器上是根本无效的,一定是-server,必须是-server。

5. 逆优化的思路

JVM与上述诉求相冲的几个地方:

各种吃内存

各种后台线程

JIT时CPU表现狂野

GC时CPU表现狂野

那我们就从这几个方面着手。

在开始折腾前,先准备好测试手段:

首先,给工具脚本配上GC 日志参数,在GC日志里就能看到实际启动参数,GC纪录,以及运行结束时内存各代的占用。

-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime

其次,长期跑一个 pidstat -l 1| grep xxx ,紧密监控进程的CPU消耗。

最后,jstack看线程。

6. 类的加载和编译优化

6.1 -Xverify:none

来自优化Eclipse启动速度的经验,说关闭Java类加载验证可以加快10% -15%的启动速度,嗯,好,加。

6.2 设定编译级别

JIT编译之后的代码比解释执行字节码更快,更省CPU。比如vjtools里的vjtop,没编译时每回运行要50%单核CPU,75ms执行完一个探测循环,而编译后10%单核,15ms就能完工。

但编译本身就需要CPU,也需要额外的编译线程。

如果脚本只简单的跑一次,比如vjtools里的vjmxcli,建议就不要进行JIT编译了,编译完了也用不上,直接解释执行就好。禁止它:-Djava.compiler=NONE

如果脚本用于密集计算,比如vjtools里的vjmap,则建议打开多层编译,一开始就对运行到的方法进行静态编译,不用等方法被调用1万次。多层编译在JDK8默认打开,显式打开:-XX:+TieredCompilation。

但打开多层编译也会导致程序运行初期有较多的编译任务,吃比较多的CPU,可以显式关掉多层编译-XX:-TieredCompilation来对比一下,综合其带来的性能提升,脚本的运行时间的长短,以及额外的CPU支出来综合评价。

6.3 编译线程的设定

在24核服务器上,默认有4条C1编译线程,8条C2编译线程(多层编译下),可以把它设到最小的-XX:CICompilerCount=2。

6.4 未来黑科技-AOT

JIT真的不适合脚本,还是预先把代码编译(Ahead-of-Time,AOT) 更好。 JDK9里有一个Hotspot编译器组搞的试验性的jaotc,另一个选择是GraalVM全家桶里带的SubstrateVM,支持JDK8。

看各位大大炫,但我还没玩过。

7. GC 优化

脚本们一般不介意GC延时,建议使用吞度量最的串行收集算法-XX:+UseSerialGC,避免了其他GC算法所需的大量GC线程,更绝对保证了自己GC时不会影响到主应用。

如果依然想使用并行算法,就一定要设置GC线程数,在24核机器上YGC和CMS GC的线程数默认分别是18和5,为了避免成为恶邻A君。可设为:

-XX:ParallelGCThreads=4 -XX:ConcGCThreads=2

8. 内存优化

首先,JVM的堆内存

一,默认的JVM初始内存大小,在大内存的服务器上会比较大,必须设置。

二,-Xms 与 -Xmx 不等时,自动扩张并没有想象中那么智能和合理。

三、新生代默认只有1/3堆大小,而在脚本看来新生代才是大头。

建议根据GC日志的结果,完整设置-Xms 和 -Xmx,并用-Xmn(新生代占大头) 或-XX:NewRatio=1(一半半) 来设置新生代大小。

其次,每条线程的内存,从默认1M回到256k:-xss256k

其他永久代,CodeCache的初始值还算合理,没看到特别浪费的情况不用管。

9. 小结

想起写这个,主要是看杨晓峰的《Java核心技术36讲》里的某几篇挑起的兴趣。

一段快速执行,关闭了JIT的脚本,启动命令大概长这个样子:

一段辅助程序,启动命令大概长这个样子

这篇的原主题是《快速,低成本,低扰动地运行一段Java代码》,以下配图刚好:

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180809G17CJD00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券