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

在线和OOM,我太难了……

时间:2023-03-16 15:17:56 科技观察

本文转载自微信公众号《石山的建筑笔记》,作者中华石山。转载本文请联系石山架构笔记公众号。今天和大家分享一次我们在基于dubbo开发线上系统时遇到的内存泄漏生产问题的排查和优化的实践经验。相信如果大家多看一些类似的案例,以后在自己的线上系统遇到各种生产问题时,对大家的启发会很大,排查优化的思路也会受到很大的启发。事故背景让我简单介绍一下这个问题的背景。在线生产环境部署了两个系统。我们可以认为它们是系统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后,发现大量对象无法回收。这个时候怎么办?我只能把这些对象转移到老年代。age已经过去了,如下图所示:此时,年轻代中大量存活的对象已经转移到老年代,老年代也快满了。填满,年轻代中大量存活的对象该去哪里呢?这个时候你去老一代?老年代充满了幸存的对象。即使触发了老年代的gc,它们也无法被回收,年轻代也没有地方放。这些幸存的物体,此时会发生什么?很简单,因为瞬时并发流量太大,同时创建的存活对象太多,把老年代和年轻代填满了,我们很可能会收到告警说jvm年轻代和内存老年代使用率超过90%。而且,这些对象都是活的,不能回收。如果此时要创建新的对象,没有创建的地方,就会报oom内存溢出异常。如下图所示:那么瞬时流量暴增可能会导致系统A的内存使用率超过90%,很快就会出现oom的问题,但是是这个问题引起的吗?虽然我们可以很顺利的推导出上面的场景,但是这个时候我们快速的看了看A系统的线上QPS指标监控,迷迷糊糊的发现A系统根本没有流量暴增,流量其他的都是稳定的,根本不是这个原因造成的问题。既然不是这个问题,那么还有什么问题会导致这个现象呢?很简单,第二个问题就是内存泄漏,也就是说在某些特殊情况下,会触发内存泄漏行为,就是你的系统一直在生成某种类型的对象,而这种类型的对象显然已经不再使用了,但是结果仍然保存在内存中,根本无法回收。如果像这样不断地积累这样的对象,内存占用会不断上升,最终导致oom内存溢出。如下图所示:那么对于这个内存泄漏问题,此时我们应该如何排查呢?这很简单。这时候不管你是真程序员还是假程序员,都得拿出真本事来。经常针对这种内存问题,使用jmap命令生成在线运行的系统jvm进程的内存dump快照,然后将dump快照下载到本地,使用MAT工具分析内存快照。在MAT工具中,我们会看到你的jvm中是什么坏掉的对象占用了这么多空间,导致你的内存占用飙升到90%+。这时候导致内存泄露的原因其实有很多。比如你自己的代码写的不好,就是每次请求的时候都创建某类对象,然后把这类对象丢到某个类的静态映射中。永不回收,也不能回收,导致这种无用的对象不断增长,最后导致oom。另一个普遍现象是我们的系统使用了一些开源框架。这些开源框架在特殊场景下创建了一堆对象,无法回收。它们从不回收自己,这就导致了开源框架QuietMimi创建的一批对象占用大量内存,导致内存泄漏。所以在这里我想跟大家说说我们当时遇到的一个问题,大家要重点吸收和排查思路。以下具体案例可作为示例。调查案例就我们当时的案例来说,经过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协议而产生大量无法回收的无用对象。综上所述,希望看完今天的产排查优化案例,如果您以后在工作中遇到类似的问题,希望能给您提供排查思路,对您有所帮助。