当前位置: 首页 > 科技观察

线上生产环境JVM内存泄露,我熬夜处理一通宵总结了一下经验

时间:2023-03-21 20:05:44 科技观察

线上生产环境JVM内存泄漏,我熬夜处理了一通通宵,总结出经验思路会很有启发。事故背景让我简单介绍一下这个问题的背景。在线生产环境部署了两个系统。我们可以认为它们是系统A和系统B。同时,由于系统B是一个核心系统,流量很大,所以部署了几十个系统。机器的定位是集群部署要抗住每秒几万TPS。两个系统都是基于dubbo作为rpc调用框架,注册中心使用zookeeper。如下图所示:在这样的背景下,某天B系统因为代码更新,发起了几十台机器的全面滚动更新部署。也就是说,B系统的开发团队根据最新的代码,重新部署了几十台最新代码的机器,也就是每台机器都会有一个系统停止和重启的过程。如下图所示:没想到生产环境的灾难性故障来的这么突然。B系统的几十台机器被一台一台重新部署后,A系统的开发团队惊讶的发现他们的系统没过多久就崩溃了。发出了jvm内存占用飙升90%以上的告警,很快A系统就因为OOM内存溢出直接崩溃了。如下图:于是B系统的开发团队在成功更新了几十台机器大版本之后,对自己的成绩很满意,但是A系统的开发团队却突然开始慌了,生产失败了。调查。所以大家可以想一想,这个时候,如果你负责的线上系统突然给你发来内存使用率飙升90%以上,很快oom内存溢出,你会怎么排查?排错思路这里给大家说说我们当时是如何排错的。首先遇到这种内存突然飙升然后导致oom的情况,先给你排查一下是不是外部请求流量过大导致的。由于此类突发问题往往是外部流量突然激增导致的,这里分析一个外部流量突然激增导致系统OOM的场景。假设你正常操作的时候,每有一批请求过来,你的jvm年轻代就会创建一批对象,然后处理这批请求,之前创建的这批对象就会变成垃圾对象,并且然后下一批请求来了,在jvm年轻代创建了一批对象。如下图所示:一般情况下,你的jvm年轻代的对象肯定越来越多吧?但实际上,在某个时刻,年轻代中存活的对象基本上很少,因为大部分对象都是之前处理过的请求创建的对象,实际上是一些无用的垃圾对象。所以其实正常情况下运行一段时间后,就会触发jvm年轻代的垃圾回收,只是回收所有的垃圾对象。如下图所示:所以正常情况下是不会有问题的,但是如果突然遇到大流量攻击怎么办?这个时候不好说,因为很有可能短时间内突然涌入大量的请求。这些请求创建了大量的对象,瞬间填满了新生代。那么此时触发年轻代GC后,发现大量对象无法回收,那么这时候怎么办呢?我们只能将这些对象转移到老年代,如下图所示:此时年轻代中大量存活的对象已经转移到老年代,老年代快满了,然后年轻一代因为流量太大,瞬间又被填满了。这时候年轻代中大量存活的对象该怎么办呢?你这时候去养老吗?老年代充满了幸存的对象。即使触发了老年代的gc,它们也无法被回收,年轻代也没有地方放这些存活下来的对象。这时候会发生什么?很简单,因为瞬时并发流量太大,同时创建的存活对象太多,把老年代和年轻代都填满了,我们很可能会收到告警说jvmyoung的内存占用代和老年代超过90%。而且,这些对象都是活的,不能回收。如果此时要创建新的对象,没有创建的地方,就会报oom内存溢出异常。如下图所示:那么瞬时流量暴增可能会导致系统A的内存使用率超过90%,很快就会出现oom问题,但是是不是这个问题导致的呢?虽然我们可以很顺利的推断出上面的场景,但是我们此时很快查看了A系统的线上QPS指标监控,发现A系统根本没有出现流量暴增的情况,其他人的流量都很稳定,所以根本不是这个原因造成的问题。既然不是这个问题,那么还有什么问题会导致这个现象呢?很简单,第二类问题就是内存泄漏,也就是说在某些特殊情况下,会触发内存泄漏行为,就是你的系统一直在生成某类对象,明明没有用到,结果还在内存中,根本无法回收。如果像这样不断地积累这样的对象,内存占用会不断上升,最终导致oom内存溢出。如下图所示:那么对于这个内存泄漏问题,此时我们应该如何排查呢?这很简单。这时候不管你是真程序员还是假程序员,都得拿出真本事来。经常出现这种内存问题,使用jmap命令生成在线运行的系统jvm进程的内存dump快照,然后将dump快照下载到本地,使用MAT工具分析内存快照。在MAT工具中,我们会看到你的jvm中是什么坏掉的对象占用了这么多空间,导致你的内存占用飙升到90%+。这时候导致内存泄露的原因其实有很多。比如你自己的代码写的不好,就是每次请求的时候都创建某类对象,然后把这类对象丢到某个类的静态映射中。永不回收,也不能回收,导致这种无用的对象不断增长,最后导致oom。另一个普遍现象是我们的系统使用了一些开源框架。这些开源框架在特殊场景下创建了一堆对象,无法回收。它们从不回收自己,这导致开源框架安静咪咪创建的一批对象占用大量内存,导致内存泄漏。所以在这里我想跟大家说说我们当时遇到的一个问题,大家要重点吸收和排查思路。以下具体案例可作为示例。调查案例就我们当时的案例来说,经过MAT的深入调查,发现占用大量内存的对象是dubbo框架创建的。dubbo框架为rpc调用创建了一个大对象,这个类型的对象已经被创建和生长,然后再也没有被回收,最后导致内存泄漏和内存不足。如下图所示:那么为什么dubbo框架一直在创建一个对象类来进行rpc调用呢?这就需要分析dubbo框架的源码。当时在分析dubbo框架的源码后,得出如下问题发生过程:当系统B上线几十台机器时,每台机器都被释放,会导致注册中心感知到服务变化,然后注册中心会将这几十台机器的地址列表推送到系统A。也就是说,连续释放几十台机器会导致注册中心推送几十次最新的地址列表,每次推送都包含几十台机器。因此,假设B系统部署了50台机器,相当于按顺序重新发布了50台机器,这样就会导致注册中心向A系统推送总共50*50=2500个机器地址。如下图所示:系统A的dubbo框架会在短时间内收到上千个被频繁推送的机器地址,然后对于每一个机器地址,dubbo框架都会实际创建一个对应的rpc调用对象班级。如下图所示:其实dubbo创建上千个rpc调用对象是没问题的,问题出在一个特例上。即系统B没有设置对外提供的rpc协议,因为dubbo支持很多不同的rpc协议,比如dubbo协议,http协议等等。所以在当时比较老的dubbo版本中,有一个隐藏的问题,就是如果B系统没有设置对外提供的特定协议版本,会导致A系统除了创建dubbo协议外,还会收到上千个机器地址对象,并基于httprest类协议创建了数千个rpc调用对象。但是B系统没有提供httprest接口,所以所有的创建都会失败,但是后面创建的大量对象会被留下,无法回收。这导致dubbo框架不断地创建大量的对象,占用了90%的内存,最后导致内存溢出。如下图所示:那么如何解决这个问题呢?其实,问题的核心在于排查思路和背后的原理,而最终解决问题往往是个案。比如我们的例子,其实很简单。就是让系统B设置对外提供的dubbo协议,避免上面那种因为没有设置protocol协议而产生大量无法回收的无用对象。综上所述,希望看完今天的产排查优化案例,如果您以后在工作中遇到类似的问题,希望能给您提供排查思路,对您有所帮助。