高并发场景下JVM调优实践

news2024/12/28 18:37:32

一、背景

2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验。

通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图:

可以看出,在观察周期里:

  • 平均每10分钟Young GC次数66次,峰值为470次;

  • 平均每10分钟Full GC次数0.25次,峰值5次;

可见Full GC非常频繁,Young GC在特定的时段也比较频繁,存在较大的优化空间。由于对GC停顿的优化是降低接口的P99时延一个有效的手段,所以决定对该核心服务进行JVM调优。

二、优化目标

  • 接口P99时延降低30%

  • 减少Young GC和Full GC次数、停顿时长、单次停顿时长

由于GC的行为与并发有关,例如当并发比较高时,不管如何调优,Young GC总会很频繁,总会有不该晋升的对象晋升触发Full GC,因此优化的目标根据负载分别制定:

目标1:高负载(单机1000 QPS以上)

  • Young GC次数减少20%-30% ,Young GC累积耗时不恶化;

  • Full GC次数减少50%以上,单次、累积Full GC耗时减少50%以上,服务发布不触发Full GC。

目标2:中负载(单机500-600)

  • Young GC次数减少20%-30% ,Young GC累积耗时减少20%;

  • Full GC次数不高于4次/天,服务发布不触发Full GC。

目标3:低负载(单机200 QPS以下)

  • Young GC次数减少20%-30% ,Young GC累积耗时减少20%;

  • Full GC次数不高于1次/天,服务发布不触发Full GC。

三、当前存在的问题

当前服务的JVM配置参数如下:

-Xms4096M -Xmx4096M -Xmn1024M-XX:PermSize=512M-XX:MaxPermSize=512M

单纯从参数上分析,存在以下问题:

未显示指定收集器 

JDK 8默认搜集器为ParrallelGC,即Young区采用Parallel Scavenge,老年代采用Parallel Old进行收集,这套配置的特点是吞吐量优先,一般适用于后台任务型服务器。

比如批量订单处理、科学计算等对吞吐量敏感,对时延不敏感的场景,当前服务是视频与用户交互的门户,对时延非常敏感,因此不适合使用默认收集器ParrallelGC,应选择更合适的收集器。

Young区配比不合理

当前服务主要提供API,这类服务的特点是常驻对象会比较少,绝大多数对象的生命周期都比较短,经过一次或两次Young GC就会消亡。

再看下当前JVM配置

整个堆为4G,Young区总共1G,默认-XX:SurvivorRatio=8,即有效大小为0.9G,老年代常驻对象大小约400M。

这就意味着,当服务负载较高,请求并发较大时,Young区中Eden + S0区域会迅速填满,进而Young GC会比较频繁。

另外会引起本应被Young GC回收的对象过早晋升,增加Full GC的频率,同时单次收集的区域也会增大,由于Old区使用的是ParralellOld,无法与用户线程并发执行,导致服务长时间停顿,可用性下降, P99响应时间上升。

未设置

-XX:MetaspaceSize和-XX:MaxMetaspaceSize​​​​​​​

Perm区在jdk 1.8已经过时,被Meta区取代,因此-XX:PermSize=512M -XX:MaxPermSize=512M配置会被忽略,真正控制Meta区GC的参数为-XX:MetaspaceSize:Metaspace初始大小,64位机器默认为21M左右 -XX:MaxMetaspaceSize:Metaspace的最大值,64位机器默认为18446744073709551615Byte,可以理解为无上限 -XX:MaxMetaspaceExpansion:增大触发metaspace GC阈值的最大要求 -XX:MinMetaspaceExpansion:增大触发metaspace GC阈值的最小要求,默认为340784Byte

这样服务在启动和发布的过程中,元数据区域达到21M时会触发一次Full GC (Metadata GC Threshold),随后随着元数据区域的扩张,会夹杂若干次Full GC (Metadata GC Threshold),使服务发布稳定性和效率下降。

此外如果服务使用了大量动态类生成技术的话,也会因为这个机制产生不必要的Full GC (Metadata GC Threshold)。

四、优化方案/验证方案

上面已分析出当前配置存在的较为明显的不足,下面优化方案主要先针对性解决这些问题,之后再结合效果决定是否继续深入优化。

当前主流/优秀的搜集器包含:

  • Parrallel Scavenge + Parrallel Old:吞吐量优先,后台任务型服务适合;

  • ParNew + CMS:经典的低停顿搜集器,绝大多数商用、延时敏感的服务在使用;

  • G1:JDK 9默认搜集器,堆内存比较大(6G-8G以上)的时候表现出比较高吞吐量和短暂的停顿时间;

  • ZGC:JDK 11中推出的一款低延迟垃圾回收器,目前处在实验阶段;

结合当前服务的实际情况(堆大小,可维护性),我们选择ParNew + CMS方案是比较合适的。

参数选择的原则如下:

1)Meta区域的大小一定要指定,且MetaspaceSize和MaxMetaspaceSize大小应设置一致,具体多大要结合线上实例的情况,通过jstat -gc可以获取该服务线上实例的情况。​​​​​​​

# jstat -gc 31247S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT37888.0 37888.0 0.0 32438.5 972800.0 403063.5 3145728.0 2700882.3 167320.0 152285.0 18856.0 16442.4 15189 597.209 65 70.447 667.655

可以看出MU在150M左右,

因此-XX:MetaspaceSize=256M 

-XX:MaxMetaspaceSize=256M是比较合理的。

2)Young区也不是越大越好

当堆大小一定时,Young区越大,Young GC的频率一定越小,但Old区域就会变小,如果太小,稍微晋升一些对象就会触发Full GC得不偿失。

如果Young区过小,Young GC就会比较频繁,这样Old区就会比较大,单次Full GC的停顿就会比较大。因此Young区的大小需要结合服务情况,分几种场景进行比较,最终获得最合适的配置。

基于以上原则,以下为4种参数组合:

1.ParNew +CMS,Young区扩大1倍​​​​​​​

-Xms4096M -Xmx4096M -Xmn2048M-XX:MetaspaceSize=256M-XX:MaxMetaspaceSize=256M-XX:+UseParNewGC-XX:+UseConcMarkSweepGC-XX:+CMSScavengeBeforeRemark

2.ParNew +CMS,Young区扩大1倍,

去除-XX:+CMSScavengeBeforeRemark

(使用【-XX:CMSScavengeBeforeRemark】参数可以做到在重新标记前先执行一次新生代GC)。

因为老年代和年轻代之间的对象存在跨代引用,因此老年代进行GC Roots追踪时,同样也会扫描年轻代,而如果能够在重新标记前先执行一次新生代GC,那么就可以少扫描一些对象,重新标记阶段的性能也能因此提升。)

-Xms4096M -Xmx4096M -Xmn2048M-XX:MetaspaceSize=256M-XX:MaxMetaspaceSize=256M-XX:+UseParNewGC-XX:+UseConcMarkSweepGC

3.ParNew +CMS,Young区扩大0.5倍​​​​​​​

-Xms4096M -Xmx4096M -Xmn1536M-XX:MetaspaceSize=256M-XX:MaxMetaspaceSize=256M-XX:+UseParNewGC-XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark

4.ParNew +CMS,Young区不变​​​​​​​

-Xms4096M -Xmx4096M -Xmn1024M-XX:MetaspaceSize=256M-XX:MaxMetaspaceSize=256M-XX:+UseParNewGC-XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark

下面,我们需要在压测环境,对不同负载下4种方案的实际表现进行比较,分析,验证。

4.1 压测环境验证/分析

高负载场景(1100 QPS)GC表现

可以看出,在高负载场景,4种ParNew + CMS的各项指标表现均远好于Parrallel Scavenge + Parrallel Old。其中:

  • 方案4(Young区扩大0.5倍)表现最佳,接口P95,P99延时相对当前方案降低50%,Full GC累积耗时减少88%, Young GC次数减少23%,Young GC累积耗时减少4%,Young区调大后,虽然次数减少了,但Young区大了,单次Young GC的耗时也大概率会上升,这是符合预期的。

  • Young区扩大1倍的两种方案,即方案2和方案3,表现接近,接口P95,P99延时相对当前方案降低40%,Full GC累积耗时减少81%, Young GC次数减少43%,Young GC累积耗时减少17%,略逊于Young区扩大0.5倍,总体表现不错,这两个方案进行合并,不再区分。

Young区不变的方案在新方案里,表现最差,淘汰。所以在中负载场景,我们只需要对比方案2和方案4。

中负载场景(600 QPS)GC表现

可以看出,在中负载场景,2种ParNew + CMS(方案2和方案4)的各项指标表现也均远好于Parrallel Scavenge + Parrallel Old。

  • Young区扩大1倍的方案表现最佳,接口P95,P99延时相对当前方案降低32%,Full GC累积耗时减少93%, Young GC次数减少42%,Young GC累积耗时减少44%;

  • Young区扩大0.5倍的方案稍逊一些。

综合来看,两个方案表现十分接近,原则上两种方案都可以,只是Young区扩大0.5倍的方案在业务高峰期的表现更佳,为尽量保证高峰期服务的稳定和性能,目前更倾向于选择ParNew + CMS,Young区扩大0.5倍方案。

4.2 灰度方案/分析

为保证覆盖业务的高峰期,选择周五、周六、周日分别从两个机房随机选择一台线上实例,线上实例的指标符合预期后,再进行全量升级。

目标组  xx.xxx.60.6

采用方案2,即目标方案

-Xms4096M -Xmx4096M -Xmn1536M-XX:MetaspaceSize=256M-XX:MaxMetaspaceSize=256M-XX:+UseParNewGC-XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark

对照组1  xx.xxx.15.21

采用原始方案

-Xms4096M -Xmx4096M -Xmn1024M-XX:PermSize=512M-XX:MaxPermSize=512M

对照组2  xx.xxx.40.87

采用方案4,即候选目标方案

-Xms4096M -Xmx4096M -Xmn2048M-XX:MetaspaceSize=256M-XX:MaxMetaspaceSize=256M-XX:+UseParNewGC-XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark

灰度3台机器。

我们先分析下Young GC相关指标:

Young GC次数

Young GC累计耗时

Young GC单次耗时

可以看出,与原始方案相比,目标方案的YGC次数减少50%,累积耗时减少47%,吞吐量提升的同时,服务停顿的频率大大降低,而代价是单次Young GC的耗时增长3ms,收益是非常高的。

对照方案2即Young区2G的方案整体表现稍逊与目标方案,再分析Full GC指标。

老年代内存增长情况

Full GC次数

Full GC累计/单次耗时

与原始方案相比,使用目标方案时,老年代增长的速度要缓慢很多,基本在观测周期内Full GC发生的次数从155次减少至27次,减少82%,停顿时间均值从399ms减少至60ms,减少85%,毛刺也非常少。

对照方案2即Young区2G的方案整体表现逊于目标方案。到这里,可以看出,目标方案从各个维度均远优于原始方案,调优目标也基本达成。

但细心的同学会发现,目标方案相对原始方案,"Full GC"(实际上是CMS Background GC)耗时更加平稳,但每个若干次"Full GC"后会有一个耗时很高的毛刺出现,这意味这个用户请求在这个时刻会停顿2-3s,能否进一步优化,给用户一个更加极致的体验呢?

4.3 再次优化

这里首先要分析这现象背后的逻辑。

对于CMS搜集器,采用的搜集算法为Mark-Sweep-[Compact]。

CMS搜集器GC的种类:

CMS Background GC

这种GC是CMS最常见的一类,是周期性的,由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,采用的是Mark-Sweep方式,由于没有Compact这种耗时操作,且可以与用户进程并行,所以CMS的停顿会比较低,GC日志中出现GC (CMS Initial Mark)字样就代表发生了一次CMS Background GC。

Background GC由于采用的是Mark-Sweep,会导致老年代内存碎片,这也是CMS最大的弱点。

CMS Foreground GC

这种GC是CMS搜集器里真正意义上的Full GC,采用Serial Old或Parralel Old进行收集,出现的频率就较低,当往往出现后就会造成较大的停顿。

触发CMS Foreground GC的场景有很多,场景的如下:

  • System.gc();

  • jmap -histo:live pid;

  • 元数据区域空间不足;

  • 晋升失败,GC日志中的标志为ParNew(promotion failed);

  • 并发模式失败,GC日志中的标志为councurrent mode failure字样。

不难推断,目标方案中的毛刺是晋升失败或并发模式失败造成的,由于线上没有开启打印gc日志,但也无妨,因为这两种场景的根因是一致的,就是若干次CMS Backgroud GC后造成的老年代内存碎片。

我们只需要尽可能减少由于老年代碎片触发晋升失败、并发模式失败即可。

CMS Background GC由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,该阈值由-XX:CMSInitiatingOccupancyFraction; 

-XX:+UseCMSInitiatingOccupancyOnly两个参数控制,不设置,默认首次为92%,后续会根据历史情况进行预测,动态调整。

如果我们固定阈值的大小,将该阈值设置为一个相对合理的值,既不使GC过于频繁,又可以降低晋升失败或并发模式失败的概率,就可以大大缓解毛刺产生的频率。

目标方案的堆分布如下:

  • Young区 1.5G

  • Old区 2.5G

  • Old区常驻对象 约400M

按经验数据,75%,80%是比较折中的,因此我们选择-XX:CMSInitiatingOccupancyFraction=75 -

XX:+UseCMSInitiatingOccupancyOnly进行灰度观察(我们也对80%的场景做了对照实验,75%优于80%)。

最终目标方案的配置为:

-Xms4096M -Xmx4096M -Xmn1536M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly

如上配置,灰度 xx.xxx.60.6 一台机器;

从再次优化的结果上看,CMS Foreground GC引起的毛刺基本消失,符合预期。

因此,视频服务最终目标方案的配置为;

-Xms4096M -Xmx4096M -Xmn1536M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSScavengeBeforeRemark -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly

五、结果验收

灰度持续7天左右,覆盖工作日与周末,结果符合预期,因此符合在线上开启全量的条件,下面对全量后的结果进行评估。

Young GC次数

Young GC累计耗时

单次Young GC耗时

从Young GC指标上看,调整后Young GC次数平均减少30%,Young GC累积耗时平均减少17%,Young GC单次耗时平均增加约7ms,Young GC的表现符合预期。

除了技术手段,我们也在业务上做了一些优化,调优前实例的Young GC会出现明显的、不规律的(定时任务不一定分配到当前实例)毛刺,这里是业务上的一个定时任务,会加载大量数据,调优过程中将该任务进行分片,分摊到多个实例上,进而使Young GC更加平滑。

Full GC单次/累积耗时

从"Full GC"的指标上看,"Full GC"的频率、停顿极大减少,可以说基本上没有真正意义上的Full GC了。

核心接口-A (下游依赖较多) P99响应时间,减少19%(从 3457 ms下降至 2817 ms);

核心接口-B (下游依赖中等)  P99响应时间,减少41%(从 1647ms下降至 973ms);

核心接口-C (下游依赖最少) P99响应时间,减少80%(从 628ms下降至 127ms);

综合来看,整个结果是超出预期的。Young GC表现与设定的目标非常吻合,基本上没有真正意义上的Full GC,接口P99的优化效果取决于下游依赖的多少,依赖越少,效果越明显。

六、写在最后

由于GC算法复杂,影响GC性能的参数众多,并且具体参数的设置又取决于服务的特点,这些因素都很大程度增加了JVM调优的难度。

本文结合视频服务的调优经验,着重介绍调优的思路和落地过程,同时总结出一些通用的调优流程,希望能给大家提供一些参考。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/472507.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【WinForm】定时器的使用方法除了定时还有延迟执行可用

在使用VS开发工具创建的WinForm项目中,有一个定时器组件,拖出来放上,它只是一个定时处理的作用,不会显示在窗体中。 开发中如果需要定时处理,就使用Timer组件即可, 在它的属性事件一栏里,有一…

offer选择:创业公司 VS 大厂外包

面试拿到两个offer,一个是规模只有几十人的初创小公司,另一个是大厂外包岗位。都是功能测试,两者薪水待遇也差不多,该如何选择?更有利于之后的职业发展...... 这是一个比较典型的问题,对于要转行的同学或者是刚入行没…

【Call for papers】2023年CCF人工智能会议信息汇总(持续更新)

本博文是根据2022年CCF会议推荐的人工智能领域相关会议目录撰写。 注: 由于一些会议的投稿时间还没公开,因此根据往年投稿时间在表格中使用 ~ 符号表示大概的投稿时间(一旦会议日期更新,我们也将同步更新博文。若更新不及时请小伙…

C++常用23种设计模式总结(一)------单例模式

什么是单例模式 单例模式是一种设计模式,它保证一个类只有一个实例,并提供一个全局访问点来访问该实例。这个模式通常用于控制资源的访问,例如数据库连接、线程池等。单例模式通过限制实例化操作并提供访问方法,确保在整个应用程序…

【Unity3D小功能】Unity3D中实现模型的旋转、缩放效果(控制摄像机)

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 一、前言 其实之前已经写了关于如何控制模型的旋转、移动、缩放效果&…

Android开发:使用sqlite数据库实现记单词APP

一、功能与要求 实现功能:设计与开发记单词系统的四个界面,分别是用户登录、用户注册、单词操作以及忘记密码。 指标要求:通过用户登录、用户注册、单词操作、忘记密码掌握界面设计的基础,其中包括界面布局、常用控件、事件处理等…

NPN三极管放大原理

NPN三极管放大 这是华为的芯片,还有其他人的芯片 无论这些芯片再复杂,它们都是由这种材料制成的 ​ 硅晶体 我们把他放大,单个硅原子最外层带有四个电子, 在纯硅当中,这些电子会两两形成共价键,此时周围形成非常稳定的八电子结构 我们接上电池, 电池无法吸引其中的电子离开,也…

不止大模型,亚马逊云科技布局AIGC底座能力

“大模型只是客户需求的其中一个部分,但远远不是所有,客户还需要更广泛的基础能力。亚马逊云科技推出自研芯片、生成式AI服务Bedrock以及大模型Titan,都在致力于推动AIGC技术的普惠化,够降AIGC的技术门槛和资金门槛,让…

万应低代码4月重点更新内容速递

低代码开发能力提升 分布式缓存支持 万应提供了一套分布式的缓存系统,可在多个节点之间共享缓存数据,从而加速数据获取速度,提高应用的性能和效率。包含: 缓存设置节点:把需要高频访问的数据写入到缓存中,并…

vue3 vue.config.js配置Element-plus组件和Icon图标实现按需自动引入

打包时,报警告,提示包太大会影响性能 1.配置前包体积: 2.安装插件: npm i unplugin-auto-import unplugin-vue-components unplugin-icons -D 3.vue.config.js中加入以下配置: const { defineConfig } require(vu…

Vue3 Antd 父子嵌套子表格

Vue3 Antd 父子嵌套子表格 父子嵌套子表格 目标1:可以点击多个父节点表格,正确显示子表格数据 目标2:父表格数据刷新重载,解决子表格数据不刷新问题 官方示例代码,以及效果 https://www.antdv.com/components/tabl…

linux0.12-8-2-asm.s

[290页] 8-2 asm.s程序 8-2-1 功能描述 1、 我们先考虑c)、 d) 2、无出错码 2.1、 将要执行的处理函数压栈; 2.2、 eax被交换入栈。现在eax护理函数 2.3、 其他寄存压栈 2.4、 立即数0压栈 2.5、 取edxEIP后,将edx压栈 2.6、 段寄存器都设置0x10段选…

通过身份个性化网络(IPM)实现真实世界的自动化妆

来源:投稿 作者:小灰灰 编辑:学姐 论文标题: Real-World Automatic Makeup via Identity Preservation Makeup Net 论文链接:https://www.ijcai.org/proceedings/2020/0091.pdf论文代码:https://github.co…

XRSPACE 总经理刘冠廷:元宇宙行业如何通过 2D、3D 联动,实现高速用户增长?

序言: 元宇宙领域创业并非坦途,似乎已经成为了行业共识。 即使到今天,VR/AR 领域的装备开支和上手学习成本居高不下,全球整体用户体量相比移动互联网也仍属早期阶段。 在这样的背景下,元宇宙公司如何持续且快速地获…

python毕业设计之django+vue流浪宠物动物救助领养网站

开发语言:Python 框架:django Python版本:python3.7.7 数据库:mysql 数据库工具:Navicat 开发软件:PyCharm 网站前台: 站内新闻:针对网站的新闻信息进行展示,能够展示…

简单聊聊Echarts伪3D地图实现的相关配置

知识和技能真的是用进废退,还是得多实践,才不至于遗忘。 目录 前言 二、实现原理 三、从0开始实现 1.目录结构 2.地图JSON数据获取 3.具体实现,重头戏 3.一些常见问题的解决方法 总结 前言 本文简单来聊一聊Echarts伪3D地图的实现&a…

Java面试被问Spring哑口无言?100道Spring面试考点解析

对于开发同学来说,Spring 框架熟悉又陌生。 熟悉:开发过程中无时无刻不在使用 Spring 的知识点;陌生:对于基本理论知识疏于整理与记忆。导致很多同学面试时对于 Spring 相关的题目知其答案,但表达不够完整准确。今天展…

UE4蓝图学习篇(八)-- 角色基础移动

在C学习篇中有介绍如何使用UE4C去处理角色的基础移动,那么本篇介绍使用蓝图去处理角色的基础移动。 1、创建Character类; 添加一个SpringArmComponent组件,在其下方添加一个CameraComponent组件能够看到角色,SpringArmComponent的…

奥斯汀独家对话|从机构的「拉扯」中成长的美国加密监管

‍前言 4月25日,在美国得克萨斯州的首府奥斯汀,这座充满活力和创造力的城市,欧科云链研究院与来自哥伦比亚商学院的Austin Campbell教授就美国加密监管以及其相关话题进行了一次深入探讨。双方讨论了美国整体的监管问题、监管逻辑、最新的稳…

华为孟晚舟当值首秀:2030年AI算力将增长500倍!

作者 | 范智林 来源 | 华商观察 微信号:HuashangGC 孟晚舟当值首次亮相。 4月19日,华为副董事长、轮值董事长、CFO孟晚舟在华为第20届全球分析师大会上进行演讲,这是她当值华为轮值董事长以来的首次公开亮相。 按照华为内部规定&#xff0c…