首页 > 编程知识 正文

提升服务的30个方法,线上常见问题排查之CPU过高

时间:2023-05-04 06:57:26 阅读:169246 作者:3389

在线服务的GC问题是Java程序中的典型问题,非常考验工程师排除问题的能力。 另外,虽然几乎都是面试的必须问题,但能真正回答这个问题的人很少。 要么原理不够好,要么缺乏实战经验。

在过去的半年里,我们的广告系统多次出现与GC相关的在线问题,有Full GC过于频繁的,也有Young GC花费过多时间的。 这些问题是GC中的程序出现问题,导致服务超时,影响广告收入。

本文以FGC频繁的在线案例为例,详细介绍了GC的故障诊断过程。 同时,结合GC的工作原理给出了实践指南。 我希望能帮到你。 内容分为以下三个部分。

让我们从FGC频繁的在线案例开始

GC的运行原理介绍

解决FGC问题的实践指南

从01FGC的频繁在线案例来看,去年10月,我们的广告召回系统在程序上线后收到了FGC的频繁系统警告。 从下面的监视图可以看出,平均每35分钟进行一次FGC。 程序在线前,我们的FGC频率大概两天一次。 以下详细介绍该问题的故障排除过程。

1 .检查JVM配置,然后使用以下命令检查JVM的启动参数:

psaux|grep ' application name=ad search '

-Xms4g -Xmx4g -Xmn2g -Xss1024K

-XX:ParallelGCThreads=5

-XX: UseConcMarkSweepGC

-XX: UseParNewGC

- xx : usecmscompactatfullcollection

- xx : cmsinitiatingoccupancyfraction=80

可见,堆内存采用4G,zgdlf采用2G,老年代也采用2G,zgdlf采用ParNew采集器,老年代采用同步标记清除的CMS采集器,在老年代内存占有率达到80%时进行FGC。

此外,在jmap-heap7276 |head-n20中发现zgdlf代的Eden区为1.6G,S0和S1区均为0.2G。

2 .观察内存在的变化通过观察内存在的使用情况,发现每次FGC内存都会恢复到500M左右,从而消除了内存泄漏。

用jmap命令查看堆内存中的对象jmap -histo 7276 | head -n20命令

上图按对象占用的内存大小顺序显示了存活对象的实例数、占用的内存和类名。 第一位是int[],可以看出它所占的内存大小远远超过了其他生存对象。 现在,我们怀疑目标已被锁定在int[]上。

4 .我打算进一步dump和分析内存文件,锁定int[],然后dump内存文件,通过可视化工具进一步跟踪对象的来源。 考虑到在堆转储期间程序将暂停,从服务管理平台上卸下此节点,然后使用以下命令卸载内存:

jmap-dump:format=b,file=heap 7276

使用JVisualVM工具导入从dump输出的堆内存文件时,还会显示每个对象占用的空间。 其中,int[]占用了50%以上的内存,再往下走,可以找到int[]所属的业务对象,并发现其来自体系结构团队提供的codis基础设施组件。

5 .代码分析可疑对象为代码分析,codis基本组件生成大小约为每分钟40M的int序列,统计TP99和TP90。 数组的生命周期为1分钟。 根据步骤2,通过观察旧时代的内存变化,推测旧时代的内存也基本上每分钟增加40M以上,这40M的int序列应该是从zgdlf世代升级到旧时代的。

并进一步调查了YGC的频率监视。 从下图中可以看出,大约1分钟有8次左右的YGC。 这样,由于CMS采集器的默认分代年龄为6次,YGC在6次后仍存活的对象将升级为老的年代,但由于codis组件中大数组的生命周期为1分钟,正好满足这一要求。

至此,整个故障排除过程基本结束。 那么,为什么在程序在线之前没有发生这个问题呢? 从上图可以看出,程序在线化前的YGC频率为5次左右,但此次在线化后的YGC频率为8次左右,因此出现了这个问题。

6 .为了快速解决解决问题,我们将CMS收集器的分代年龄更改为15次。 变更后,FGC频率恢复为2天1次。 此后,如果YGC频率超过每分钟15次,此问题将再次发生。

当然,我们的根本解决方案是优化程序以降低YGC的频率,从而缩短codis组件中int数组的生命周期,但我们不会在此展开。

02GC的工作原理介绍了上述整个案例的分析过程,实际上涉及到很多GC的原理知识,不知道这些原理就着手处理,实际上整个故障排除过程是盲目的。

现选取几个最核心的知识点,介绍GC的工作原理,最后给出实践指南。

1 .堆存储器的结构众所周知, GC分为YGC和FGC,它们都发生在JVM的堆存储器中。 首先来看看JDK8的堆内存结构。

可以看出,堆内存采用了分代结

构,包括zgdlf代和老年代。zgdlf代又分为:Eden区,From Survivor区(简称S0),To Survivor区(简称S1区),三者的默认比例为8:1:1。另外,zgdlf代和老年代的默认比例为1:2。

堆内存之所以采用分代结构,是考虑到绝大部分对象都是短生命周期的,这样不同生命周期的对象可放在不同的区域中,然后针对zgdlf代和老年代采用不同的垃圾回收算法,从而使得GC效率最高。

2. YGC是什么时候触发的?

大多数情况下,对象直接在年轻代中的Eden区进行分配,如果Eden区域没有足够的空间,那么就会触发YGC(Minor GC),YGC处理的区域只有zgdlf代。因为大部分对象在短时间内都是可收回掉的,因此YGC后只有极少数的对象能存活下来,而被移动到S0区(采用的是复制算法)。

当触发下一次YGC时,会将Eden区和S0区的存活对象移动到S1区,同时清空Eden区和S0区。当再次触发YGC时,这时候处理的区域就变成了Eden区和S1区(即S0和S1进行角色交换)。每经过一次YGC,存活对象的年龄就会加1。

3. FGC又是什么时候触发的

下面4种情况,对象会进入到老年代中:

YGC时,To Survivor区不足以存放存活的对象,对象会直接进入到老年代。

经过多次YGC后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。

动态年龄判定规则,To Survivor区中相同年龄的对象,如果其大小之和占到了 To Survivor区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。

大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过zgdlf代, 直接在老年代中分配。

当晋升到老年代的对象大于了老年代的剩余空间时,就会触发FGC(Major GC),FGC处理的区域同时包括zgdlf代和老年代。除此之外,还有以下4种情况也会触发FGC:

老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发FGC。

空间分配担保:在YGC之前,会先检查老年代最大可用的连续空间是否大于zgdlf代所有对象的总空间。如果小于,说明YGC是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发Full GC;如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC。

Metaspace(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize 参数的指定值时,也会触发FGC。

System.gc() 或者Runtime.gc() 被显式调用时,触发FGC。

4. 在什么情况下,GC会对程序产生影响?

不管YGC还是FGC,都会造成一定程度的程序卡顿(即Stop The World问题:GC线程开始工作,其他工作线程被挂起),即使采用ParNew、CMS或者G1这些更先进的垃圾回收算法,也只是在减少卡顿时间,而并不能完全消除卡顿。

那到底什么情况下,GC会对程序产生影响呢?根据严重程度从高到底,我认为包括以下4种情况:

FGC过于频繁:FGC通常是比较慢的,少则几百毫秒,多则几秒,正常情况FGC每隔几个小时甚至几天才执行一次,对系统的影响还能接受。但是,一旦出现FGC频繁(比如几十分钟就会执行一次),这种肯定是存在问题的,它会导致工作线程频繁被停止,让系统看起来一直有卡顿现象,也会使得程序的整体性能变差。

YGC耗时过长:一般来说,YGC的总耗时在几十或者上百毫秒是比较正常的,虽然会引起系统卡顿几毫秒或者几十毫秒,这种情况几乎对用户无感知,对程序的影响可以忽略不计。但是如果YGC耗时达到了1秒甚至几秒(都快赶上FGC的耗时了),那卡顿时间就会增大,加上YGC本身比较频繁,就会导致比较多的服务超时问题。

FGC耗时过长:FGC耗时增加,卡顿时间也会随之增加,尤其对于高并发服务,可能导致FGC期间比较多的超时问题,可用性降低,这种也需要关注。

YGC过于频繁:即使YGC不会引起服务超时,但是YGC过于频繁也会降低服务的整体性能,对于高并发服务也是需要关注的。

其中,「FGC过于频繁」和「YGC耗时过长」,这两种情况属于比较典型的GC问题,大概率会对程序的服务质量产生影响。剩余两种情况的严重程度低一些,但是对于高并发或者高可用的程序也需要关注。

 

03 排查FGC问题的实践指南

通过上面的案例分析以及理论介绍,再总结下FGC问题的排查思路,作为一份实践指南供大家参考。

1. 清楚从程序角度,有哪些原因导致FGC? 

大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。

内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM.

程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC. (即本文中的案例)

程序BUG导致动态生成了很多新类,使得 Metaspace 不断被占用,先引发FGC,最后导致OOM.

代码中显式调用了gc方法,包括自己的代码甚至框架中的代码。

JVM参数设置问题:包括总内存大小、zgdlf代和老年代的大小、Eden区和S区的大小、元空间大小、垃圾回收算法等等。

2. 清楚排查问题时能使用哪些工具

公司的监控系统:大部分公司都会有,可全方位监控JVM的各项指标。

JDK的自带工具,包括jmap、jstat等常用命令:

# 查看堆内存各区域的使用率以及GC情况

jstat -gcutil -h20 pid 1000

# 查看堆内存中的存活对象,并按空间排序

jmap -histo pid | head -n20

# dump堆内存文件

jmap -dump:format=b,file=heap pid

可视化的堆内存分析工具:JVisualVM、MAT等

3. 排查指南

查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)

了解该时间点之前有没有程序上线、基础组件升级等情况。

了解JVM的参数设置,包括:堆空间各个区域的大小设置,zgdlf代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。

再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。

针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。

通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。

 

最后的话

这篇文章通过线上案例并结合GC原理详细介绍了FGC的排查过程,同时给出了一份实践指南。

后续会以类似的方式,再分享一个YGC耗时过长的案例,希望能帮助大家吃透GC问题排查,如果觉得本文对你有帮助,请帮忙转发或者点个再看!

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。