0
点赞
收藏
分享

微信扫一扫

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


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

一、背景

我所负责的 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=8

Eden: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 打印参数,以提供更多的信息

js 代码解读复制代码-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。

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 ,看看有没有优化空间。


举报

相关推荐

0 条评论