likes
comments
collection
share

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

作者站长头像
站长
· 阅读数 8

一、缘起

在一个阳光明媚的周五傍晚,虽然还未到周末,但我已然沉浸在这即将到来的自由时光中无法自拔。秃然,奇怪的事情发生了~

一个服务在疯狂的FULLGC报警,把我的思绪从美好的吃喝玩乐中拉回了现实,周五下班前出问题的魔咒又来了吗?

PS: 本文是线上问题系列的第四篇,主题为频繁Full GC问题的排查与解决方法论

注:本案例为中大型互联网公司遇到的真实案例(没错,就是我亲自遇到的……),其知识点的深度和广度拿来面试都足够,建议认真阅读并且熟悉涉及到的相关知识点。若有任何问题可在评论区指出~

二、奇怪的现象

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

三、案发背景&现场

在排查FULLGC问题前,尽可能的抓住所有的案发现场证据,手里有更多的牌,以便让我们应对的更自如

3.1 这是个什么服务?

首先了解下这是个什么服务?

ECP是一个将数据从不同的数据源(如文件,Mysql,TIDB)等将数据同步到ES中的服务,里面的实现逻辑总体可以解释为:

把数据从数据源拉出来,再塞到ES里面去。因此里面涉及到了大量的数据操作

里面涉及到的大量细节,如大数据量读取、SPI机制、QPS限制、索引管理等后面会出个【设计亮点系列】来描述,在这里只介绍和GC问题相关的一些流程

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

由于该服务为数据同步服务,不像业务服务一直有请求,在触发频繁Full GC的时候,当前无正在进行中的任务

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

3.2 FULLGC的情况

如下图监控平台是15秒上报一次值,15秒大概8次Full GC,频次已经相当高了,GC耗时也不短

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

如下图,Full GC是在晚上20.52突然起来的,经过查看,此时正有一个9000多w数据同步的任务正在进行中,该任务于凌晨1点结束,任务结束后Full GC依然继续,没有停下的迹象

GC图: 【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

任务图: 【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

3.3 服务机器情况

ECP总共有两台机器,只有一台机器在不断的在Full GC,而这台机器就是任务执行的机器,说明该Full GC问题和任务执行强相关

3.4 服务JVM的参数设置

  • XX:MetaspaceSize=256m: 设置Metaspace初始大小为256MB。Metaspace用于存储类的元数据,包括类的结构、方法信息等。
  • XX:MaxMetaspaceSize=256m: 设置Metaspace的最大大小为256MB。Metaspace会根据需要动态增长,但不会超过这个限制。
  • XX:+UseParNewGC: 启用ParNew垃圾收集器,用于新生代的垃圾收集。ParNew是一种多线程的垃圾收集器,用于新生代的并行收集。
  • XX:+UseConcMarkSweepGC: 启用CMS(Concurrent Mark and Sweep)垃圾收集器,用于老年代的垃圾收集。CMS是一种以最短停顿时间为目标的垃圾收集器。
  • XX:+UseCMSCompactAtFullCollection: 启用CMS在Full GC(完全垃圾回收)时进行内存碎片整理。这有助于减少内存碎片,提高内存利用率。
  • XX:CMSInitiatingOccupancyFraction=80: 设置CMS触发垃圾回收的老年代占用比例阈值为80%。当老年代占用达到这个比例时,CMS会启动垃圾回收。
  • Xms4g: 设置JVM的初始堆大小为4GB。JVM启动时分配的堆的最小值。
  • Xmx4g: 设置JVM的最大堆大小为4GB。JVM堆的最大允许大小。
  • Xmn2g: 设置新生代的大小为2GB。新生代是堆的一部分,用于分配新对象。

该服务新生代使用ParNewGC垃圾收集器,老年代使用CMS垃圾收集器 ,总堆4G,新生代2G,老年代2G,元空间256MB

3.5 JVM的内存变化情况

JVM相关区域内存变化如图:

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

四、猜想

如果一个现象发生了,那么我们需要在脑海中穷举所有的可能性,然后一一去确认(这里需要一定的知识储备哦),既然是频繁的FULLGC,那么FULLGC的触发条件是什么呢?(在老年代为CMS收集器的情况下)

  1. 老年代空间不足(CMS GC、G1 GC): 当老年代的空间不足时,JVM 会触发 Full GC,尝试回收老年代中的无用对象。
  2. 永久代或 Metaspace 空间不足: 在永久代(Java 7 及之前)或 Metaspace(Java 8+)空间不足时,也会触发 Full GC,尝试回收永久代或 Metaspace 中的无用对象。
  3. Minor GC晋升到老年代的平均大小大于老年代的剩余空间,其实也是老年代的空间不足了
  4. Cocurrent mode failure ,在执行CMS GC的过程中,如果此时有线程将对象放入老年代,并且老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间不足,需要放入老年代,而老年代空间也不足,则触发Full GC。---其实也是老年代空间不足了
  5. 系统调用 System.gc()

总结一下,Full GC的触发条件其实就是以下三大类:

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

当知晓了Full GC的触发条件,频繁Full GC也就是上面三个条件不断得被触发而已

五、猜想验证

对于第一种情况:system.gc()的调用,直接利用idea全局搜索即可,这个简单,验证完没问题直接跳过

对于第二种情况,ecp服务配置的元空间大小为256MB,一般公司都有较为完善的监控系统来观察这类值的变化,如下图,ECP服务的元空间占用为102MB,也可以排除

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

对于第三种情况,看下老年代的占用,发现已经到了最大值2g,而且回收并没有让老年代占用的内存空间变小,这样问题就比较明显了,大概率是内存泄露导致的,如下图。

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

什么是内存泄露?内存泄漏指的是程序在运行过程中,未能正确地释放不再需要使用的内存,导致这些内存无法被回收,长时间累积造成系统内存消耗过多的现象。简单来说,就是程序在使用内存时“丢失”了一部分内存,不再能够访问和释放它们。

至此频繁Full GC的问题已经明确,即老年代的使用达到阈值,因此触发Full GC进行回收,但回收完空间占用依然达到使用阈值,因此不断的触发Full GC进行回收

六、问题追溯与解决

明确了是因为内存泄露导致频繁的Full GC,那就需要定位导致内存泄露的问题代码在什么位置?那定位流程是什么呢?以下列举了追踪路径:

  1. 将节点的流量摘掉,保证没有业务访问该服务,避免影响到线上业务
  2. DUMP节点的内存快照
  3. 利用MAT工具分析内存泄露的对象
  4. 找到操作该对象的代码,利用IDEA的代码调用追溯找到调用链
  5. 审视代码调用链的逻辑

总的来说,就是找到无法回收的内存对象,看下什么地方操作了这些对象,排查下有没有问题

实战一下:

  1. 摘掉该节点的流量,这个一般公司的基础组件会提供该能力,如下图:

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

  1. 利用Arthas dump(Jmap也可)下来堆内存里面存活的对象

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

  1. 利用MAT对象分析dump下来的内存快照,检测到了内存泄露的问题

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

还可以看到对象的引用链,可以发现ZhuanZhuanRegistry里面的serviceListenersMap占用暴涨

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

  1. 找到向serviceListenersMap 存放数据的方法,利用IDEA的方法调用追溯找到整条调用链(在MAC上的快捷键是option+command+H),如下图

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

5.发现ECP服务中的一段用于推送数据的方法(reference.refer()),在每次调用的时候,其底层都会向map中put一个值,在大数据量的时候,频繁调用,最终导致内存溢出,如下图:

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

七、修复

将reference.refer()的结果放入到Map中,每次调用的时候从Map中取,取不到的时候再创建,如下图:

【面试拿来即用系列】你遇到过什么线上问题,如何解决的?(四)

至此,后续的大数据量任务再也没有Full GC的问题了,完美~

八、总结

当遇到频繁Full GC的问题时,如果已经影响到线上业务了,那就快速解决,先把一台机器的流量摘掉,其余机器重启,这样不但可以保留案发现场,并可以快速解决问题。

在摘掉流量的机器上,可以利用jmap或arthas将内存快照dump下来进行分析,以定位为什么会频繁的触发full gc~

当然我们需要对full gc的触发有系统级的理解,这样才可知彼知己,百战不殆