被狠狠夸了,我把服务的吞吐率提升了22.5%,只因为我调整了JVM参数

一、看数据图,寻找降本增效的可能性!

拉 7 天的数据看
在这里插入图片描述

Eden 区 + Survivor 区 + Old 区 图有了,看起来 Eden 的 GC 频繁,Old 有足够的空间。

在这里插入图片描述

GC 次数频繁,GC 耗时较长。

在这里插入图片描述

定位到 7 天内有 1次 Full GC 的位置。

在这里插入图片描述

CPU 使用率较低,内存使用率一般。

二、分析情况,寻找可以优化的点…

  • CPU 核可以压榨,CPU 使用率只有 10%,可以用来优化运行效率。
  • 大部分的对象在 Eden 就消亡,Old Gen 的空间有大量的冗余,并且由于 Eden 过早晋升,导致 Old Gen 内存越来越大,发生 GC,可以增加 Eden 的占比来优化。
  • 元空间冗余非常多,可以缩小一部分。
  • Survivor 区可以适当缩小比值,给 Eden 区。
  • 每分钟大概有 200ms 的 GC 耗时,吞吐率 99.67%,服务是 IO 交互性,需要提升至 99.99%。

我们的初始参数:

JAVA_OPTS="-Xmx5734m -Xms5734m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -Xmn2120m -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCDetails"

三、参数优化确定了!小步快走看效果。

  • 7.81 GB 的 Pod 内存,JVM 堆可以设置到 7 GB,线上非堆内存 280MB 我们预留 0.81GB(828MB)是足够的,所以我们可以先确定参数 -Xmx7168m
  • 活跃对象,看 Old Gen Full GC 后,剩余大概 785 MB,我们预留 3 倍(应对突发流量浮动垃圾)活跃对象空间给 Old Gen,也就是 2355 MB,剩余的给 Young Gen,确定参数 -Xmn4813m
  • 我们看生产的 Survivor 区平均占用 22.5 MB,根据 Eden 最大值 与 Survivor 最大值比值为 8(End:S0:S1 = 8:1:1),与默认比值一致,我们发现对象 99% 存活时间 < 1次 Minor GC,我们可以先缩小 S 区为 16:1:1,这样 S 区会有 267 MB 足够让对象经过 15 次 MinorGC 的过滤再到 Old,但是考虑到小步快走,先设置为 -XX:SurvivorRatio=12,后续观察。
  • 由于每次 Minor GC 的量变大,预计每次收集 2~3 G,所以收集也得加快,CPU 为 4C 规格,我们使用 CPU 总核数的线程来加速 GC(毕竟 STW,一般最好与CPU核心数量相等),并发收集参数 -XX:ParallelGCThreads=4,后续看情况再调整。
  • Meta 使用 156 MB,服务是没有动态代理业务,我们设置 256 MB 即可,-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m。剩余的非堆内存也足够 Code Cache、OS 使用了。
  • 为了更好地后续调优,加入参数 -XX:+PrintTenuringDistribution 监控晋升年龄分布。

计划的参数:

JAVA_OPTS="-Xmx7168m -Xms7168m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -Xmn4813m -XX:SurvivorRatio=12 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintTenuringDistribution"

四、压测验证,GC 次数减少 50%,接口吞吐提升 22.5%

注意 JVM 参数修改不要和发版一起,在平时时间叫运维改动即可。我们可以用测试环境配置与 Prod 相同的 K8s 配置、JVM 参数。

找资源消耗量大的核心接口进行压测,如何确定我们要压测的核心接口?我 dump 发现不出大对象接口,所以准备通过一些数据多的接口来压测,最终选用了“数据同步”接口。

压测参数:

  • Number of Threads (users): 1000: 设置 1000 个并发用户,保证系统短时间内大量内存爆涨
  • Ramp-up period (seconds): 5 :5秒内启动所有用户,给被压测系统预热的时间
  • Loop Count: 10:重复 10 轮

优化前:

在这里插入图片描述

在这里插入图片描述

指标问题等级分析
平均响应时间 Average37,880 ms⚠️ 致命平均响应时间37.8秒(用户等待超时)
最差响应时间 Max483,104 ms⚠️ 致命最差响应时间483秒(≈8分钟) 部分请求完全阻塞
吞吐量 Std. Dev.23,705.65⚠️ 高危响应时间波动剧烈(±23.7秒),系统极不稳定
错误率 Error %0.03%⚠️ 中危开始出现错误(约3个失败请求),可能是超时或资源耗尽

优化后:

在这里插入图片描述

在这里插入图片描述

指标优化前优化后变化
平均响应时间37,880 ms36,920 ms↓ 2.5%
最差响应时间483,104 ms149,293 ms↓ 69%
吞吐量18.6/sec22.8/sec↑ 22.5%
错误率0.03%0.00%归零 ✅

五、关键结论

  • Pod 可用内存可以压榨,预留空间给 OS 操作即可,其他给 Heap(-Xmx-Xms) + Meta(-XX:MetaspaceSize=-XX:MaxMetaspaceSize=),避免堆外内存溢出,要保证 Heap > NoHeap(Meta + Code Cache + …) + OS Ops。
  • IO 交互型应用,例如互联网服务接口,大多数对象都是存活极短时间,在使用了分代收集的情况下(例如 CMS),Old 的大小应当为活跃对象(Full GC 后存活的)的 2~3 倍左右,考虑到浮动垃圾问题最好在 3 倍左右,剩下的都可以分给 Young 区,这样可以减少过早晋升带来的 GC 频繁问题。
  • Young 区中,E 区和 S 区的比值(-XX:SurvivorRatio=)一般都是需要小步快走的,如果是 IO 交互型应用,S 区可以适当缩小,但是不宜过小而导致 S 区不足晋升 Old 区。
  • 每次 GC 的量变大的情况下,加大 GC 线程数,-XX:ParallelGCThreads=,一般最好与CPU核心数量相等。
  • 具体问题具体分析,JVM 调优是实战慢慢调整,慢慢经验积累的。

六、参考资料

《2020 美团技术年货-后台篇》

《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》

《剑指 JVM:虚拟机实践与性能调优》

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值