注册

【后端性能优化】接口耗时下降60%,CPU负载降低30%

大家好,我是五阳。


GC 话题始终霸占面试必问排行榜,很多人对 GC 原理了然于胸,但是苦于没有实践经验,因此本篇文章将分享我的GC 优化实践。一个很小的优化,产生了非常好的效果。


现在五阳将优化过程给大家汇报一下。




一、背景


我所负责的 A 服务每天的凌晨会定时执行一个批量任务,每天执行时都会触发 GC 频率告警,偶尔单机 CPU 负载超过 60%时,会触发 CPU 高负载告警。


曾经有考虑过通过单机限流器,限制任务执行速率,从而降低机器负载。然而因为业务上希望定时任务尽快执行完,所以优化方向就放在了如何降低 CPU 负载,如何降低 GC 频率。


1.1 配置和负载



  • 版本:java8
  • GC 回收器:ParNew + CMS
  • 硬件:8 核 16G 内存,Centos6.8
  • 高峰期CPU 平均负载(分钟)超过 50%(每个公司计算口径可能不同。我司的历史经验超过 70%后,接口性能将会快速恶化)

1.2 优化前的 GC情况


不容乐观。



  • 高峰期 Young GC频率 70次/min,单次 ygc 平均时间 125ms;
  • 高峰期 Full GC频率 每 3 分钟 1 次;单次 fgc 平均时间 610ms。

1.3 GC 参数和 JVM 配置


参数配置说明
-Xmx6g -Xms6g堆内存大小为6G
-XX:NewRatio=4老年代的大小是新生代的 4 倍,即老年代占4.8G,新生代占1.2G
-XX:SurvivorRatio=8Eden:From:To= 8:1:1,即Eden区占0.96G,两个Survivor区分别占0.12G
-XX:ParallelCMSThreads=4设置 CMS 垃圾回收器使用的并行线程数为 4
XX:CMSInitiatingOccupancyFraction=72设置老年代使用率达到 72% 时触发 CMS 垃圾回收。
-XX:+UseParNewGC启用 ParNew 作为年轻代垃圾回收器
-XX:+UseConcMarkSweepGC启用 CMS 垃圾回收器

二、问题分析


2.1 增加 GC打印参数


由于打印GC信息不足,无法分析问题。因此添加了 以下GC 打印参数,以提供更多的信息


-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps
-XX:+PrintCommandLineFlags
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintReferenceGC

2.2 提前晋升现象


配置如上参数后,每次发生 younggc后,都会打印详细的 younggc 日志。通过分析 gc 日志,我发现日志中经常出现类似内容。
Desired survivor size 61054720 bytes, new threshold 2 (max 15)


new threshold是新的晋升阈值,是指对象在新生代经过 new threshold 轮 younggc后,就能晋升到老年代,这个值通过 MaxTenuringThreshold配置,默认值是 15,在原有理解中阈值是固定值 15,实际上这个值会动态调整。



为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄



Desired survivor 一般是 Survivor 区的一半。假设年龄 1至N 的对象大小,超过了 Desired size,那么下一次 GC 的晋升阈值就会调整为 N。举个例子,假设 age=1的对象为 80M,超过了 61M,那么下一次GC 的晋升阈值就是 1,所有超过 1 的对象都会晋升到老年代,无需等到年龄到 15。


如何分析 younggc 日志,可以参考我的另一篇文章。2024面试必问:系统频繁Full GC,你有哪些优化思路?第一步分析gc日志


2.3 老年代增长速度过快


为了印证是否发生提前晋升,我通过监控查看到在事发时间,老年代内存的涨幅和 Survivor的内存基本一致,看来新生代的对象确实提前晋升到老年代了。


grep 分析历次 GC 后的晋升阈值后,我发现绝大部分情况下,新生的对象无法在 15 次 GC后进入到老年代,基本上三次以后就会提前晋升到老年代…… 这解释了为什么会发生频繁的 FullGC。


假设每次提前晋升 100M 到老年代,每分钟超过 15 次 ygc,则每分钟将会有 1.5G 对象进入老年代。


因为频繁地提前晋升,老年代的增长速度极快。 在高峰期时,往往 2 至 3 分钟左右,老年代内存就会触达 72% 的阈值,从而发生 FullGC。


2.4 新生代内存不足


即便老年代配置 4.8G 的大内存,但频繁地发生提前晋升,老年代也很快被打满。这背后的根本原因在于 新生代的内存太小了。 新生代,总共 1.2G 大小,Survivor才 120M,这远远不够。


于是我们调整了内存分配。调整后如下



  • -Xmx10g -Xms10g -Xmn6g
  • -XX:SurvivorRatio=8


  1. 堆内存由 6G 增加到 10G
  2. 大部分堆内存(6G)分配给新生代。新生代内存从 1.2G 增加到 6G。
  3. Eden:From:To 的比例依然是 8:1:1
  4. Eden大小从 0.96 G 增加到 4.8 G。
  5. Survivor区由 120 M 增加到 600 M。

三、优化效果


虽然改动不大,但是优化效果十分显著。由于公司监控有水印,我无法截图取证,敬请谅解。


3.1 GC频率明显下降



  • 高峰期 ygc 70 次/min 降到了 12 次/min,下降幅度达83%(单机 500 QPS)
  • 高峰期 fgc 三分钟1 次,降到了 每天 1 次 Full GC。
  • younggc 和 fullgc 单次平均耗时保持不变。

3.2 CPU 负载降低 30%+



  • 优化之前高峰期 cpu 平均负载超过 50%;优化后降到了不足 30%,高峰期负载下降了 40%。
  • CPU负载每日平均值 由 29%,降到了 20%。日平均负载下降了 32%。

3.3 核心接口性能显著提升


核心接口耗时下降明显



  • 接口 A 高峰期 TPS 100/秒,tp999 由 200毫秒 降到了 150 毫秒, tp9999 由 400 毫秒降到了 300 毫秒,接口耗时下降超过 25%!
  • 接口 B 高峰期QPS 250/秒, tp999 由 190 毫秒降到了 120 毫秒, tp9999 由 450 毫秒下降到了 150 毫秒,接口耗时下降分别下降 37%和 67%!
  • 接口 B 低峰期降幅更加明显,tp999 由 80 毫秒降到了 10 毫秒,下降幅度接近 90%!

后来又适当微调了 JVM 内存分配比例,但是优化效果不明显。


四、总结


经过此次 GC 优化经历,我学到了如下经验



  • 要通过 GC 日志分析 GC 问题。
  • 调整JVM 内存,保证足够的新生代内存。
  • 优化 GC 可以降低接口耗时,提高接口可用性。
  • 优化 GC 可以有效降低机器 CPU 负载,提高硬件使用率。

反过来当接口性能差、cpu负载高的时候,不妨分析一下 GC ,看看有没有优化空间。


详细了解如何分析 younggc 日志,可以参考我的另一篇文章。2024面试必问:系统频繁Full GC,你有哪些优化思路?第一步分析gc日志


关注五阳~ 了解更多我在大厂的实际经历


作者:五阳
来源:juejin.cn/post/7423066953038741542

0 个评论

要回复文章请先登录注册