JVM 调优之 Reserved Code Cache Size

张智超
2022年 2月 11日
01 现象

社区小伙伴最近在为 Kylin 4 开发 Soft Affinity + Local Cache 的性能测试过程中,遇到了压测场景下查询响应时间不稳定问题, RT 随着时间变化较大,现象如下:

  1. 同样的 SQL (只是参数不同) 在压测时,刚开始的性能较好,运行一段时间后,性能就下降严重,一段时间后(不固定多长)又有可能恢复之前的水平;
  1. Spark 重启后,性能同样也能好转;

如下图所示:

图一:测试第一阶段和第二阶段

第一阶段,测试开始,查询响应时间比较平稳,在 1 秒左右;

第二阶段,16:06 以后查询的响应时间逐渐变大,慢慢增长到第一阶段的两倍,达到接近 2 秒;

图二:测试第三阶段

第三阶段,查询的响应时间慢慢恢复之前的水平。

02 寻找 Root Cause

相同的环境,一样的配置,每次运行都能复现这种现象,且性能相差巨大,不找出 Root Cause 似乎会失眠。于是开始了一波测试,观察 GC、收集火焰图、做性能下降前后的对比,发现均没有什么太大区别,且都正常。

根因排查

1.Spark Executor GC Log 分析

图三:Executor JVM 内存使用
图四:Executor JVM GC 耗时分布

根据 GC log 分析,大部分 GC 在 20ms 内,比较正常,Peak Memory Size 也毫无压力。

2.Spark Executor 火焰图

图五:Executor 火焰图

从火焰图上看,唯一区别在于小红框部分的比例前后不同,但这部分是 Spark Code Generate 的代码,相同的 SQL ,代码逻辑是一样,大红框整体的占比也是差不多的(后来回想,这是一个异常点,只是当时无法想到是跟 JIT 有关)。

3.Parquet 读取

图六:Executor 读取文件相关部分火焰图

从火焰图上可以看到,紫色部分是读取 Parquet 数据部分,占比和正常水平时的一样。

关键线索

一切看起来都是那么的正常,但为什么却有两倍的 RT 呢?毫无头绪下,只能再回归各种指标数据,日志文件了。

在看 Executor stderr log 时,无意间看到了这个 Warning,且各个 Executor 上都有:

图七:JVM Warning Message on Executor stderr

因此开启 Code Cache Size 性能指标采集 (该性能指标似乎自己平时比较少关注),再压测一轮,发现了端倪:Code Cache 影响了查询性能。

查看 JMeter Report 中随时间变化的 RT 曲线,并且结合 Spark Metrics 的 Code Cache Size 曲线,发现 RT 翻倍的时间点与各个 Executor Code Cache Size 满的时间点吻合:

图八:Kylin 4 Response Time 曲线图
图九:Spark Executor Code Cache Size 曲线图

在 RT 高峰时,所有 Executor 的 Code Cache Size 均达到最大值,后续有些 Executor 的 Code Cache Size 因为回收机制下降下来,此时的 RT 也开始略有好转,当所有 Executor 的 Code Cache 都未达到最大值时,RT 恢复正常。

Root Cause

经过上面的分析,接着验证下是否分析正确,因此把 JVM 参数 -XX:ReservedCodeCacheSize=256M 调整为 2G ( 目前该参数最大可配置 2G ),再次压测一轮,整体 RT 就稳定了许多:

图十:调整后 Kylin 4 Response Time 曲线图

且观察 Code Cache Size 性能指标,发现最大值可达到 400M:

图十一:调整后 Spark Executor Code Cache Size 曲线图

至此,Root Cause 找到了,因为 Code Cache Size 满了,导致了 JIT 停止,而 JIT 是 JVM 提升性能的一把利器,停止了工作,随之带来的性能下降显而易见,影响巨大。

03 小结

1. Code Cache Size 的性能指标,对 JIT 功能至关重要,必须监控起来,然后根据实际使用情况,进行适当调整;

2. JVM 中 Code Cache Size 及 JIT 等相关参数:

  • -XX:+TieredCompilation:开启分层编译( C1 and C2 ),JDK 8 之后默认开启;
  • -XX:+UseCodeCacheFlushing:在JIT被关闭之前,也就是 CodeCache Size 满之前,会做一次清理,删除一些 Code Cache 的代码,如果清理后还是没有空间,那么 JIT 依然会关闭,JDK 8 默认开启;
  • -XX:ReservedCodeCacheSize=2G:Code Cache Size,最大可配置 2G;
  • -XX:NmethodSweepActivity=10:默认值 10,计算清理 compiled method 的阈值,具体可以看对应的代码:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/f22b5be95347/src/share/vm/runtime/sweeper.cpp#l588 ;值越大,在清理时会有越多的 compiled method 被清理,意味着回收会越快;
  • -XX:Tier3CompileThreshold=2000 and -XX:Tier3InvocationThreshold=200:开启分层编译后,C1 层编译和调用的阈值,值越小,越容易被 JIT 编译;
  • -XX:Tier4CompileThreshold=15000 and -XX:Tier4InvocationThreshold=5000:开启分层编译后,C2 层编译和调用的阈值,值越小,越容易被 JIT 编译;

* 以上四个参数,官方建议不要自行修改

04 参考资料

1. 基本功 | Java 即时编译器原理解析及实践

2.https://stackoverflow.com/questions/38173592/oracle-jvm-8-when-codecache-flushing-is-enabled-how-much-is-flushed

申请试用
关注我们