一次项目部署到服务器上导致内存爆了的问题排查经历

237 阅读8分钟

起因与问题定位

前天下午,前端那边突然反馈给我服务器中部署项目的接口访问不上了,连远程Swagger界面都打不开,我才加入这个项目每一个月呢,这咋就突然出事了呢?难道真是我写的代码质量太差了?但是首先还是得先排查问题,我一打开服务器,发现某个项目占据了服务器60%多的内存

image.png

使用free命令会发现可用内存还不到100M

image.png

显然问题就在于项目存在内存泄漏等问题,导致这个项目的垃圾没有被正确回收,所以占满了所有的内存

定位问题

按照我的流程,我的解决问题的主要步骤是:

  1. 重构项目。为了保证前端可以继续工作,我使用Jenkins重新构建了这个项目,重新构建之后项目就可以正常访问了。
  2. 定位问题。因为我在这个项目里也加入了我的代码,首先怀疑问题是由于我的代码产生的,因为假如这个问题不是因为我的代码而产生的,那么这个问题早就该出现了,不会是在我加入代码后不久产生的。按照这个思路我将代码回退到我进行改动之前的版本并部署到了另外一台服务器上,假如是我的代码有问题,那么没有我的代码的项目是不会出现相同情况的。
  3. 意外之喜。然而在我还在部署的过程中,我发现我之前重构的项目又吃满了内存,但是还能正常访问,我赶紧问问前端刚刚有没有发送什么请求,前端说有,我再次重构了项目,让前端重新发送一次请求,果然前端发送请求之后服务器的内存就大大减少,说明问题出现在这个接口,通过前端的请求地址发现这个接口并不是我写的接口,我当时总算松了一口气,还好不是我的锅

排查问题

排查问题。确定问题出现在这个接口里,那么说明肯定是这个接口存在问题,我用Jprofiler本地启动项目并调用该接口,看看这个接口的运行过程具体是怎么样的,可以看到调用该接口后项目的具体的运行过程如下

e10d116c894319c3742b9aea26f92e6.png

可以看到每次调用这个接口都会在最后遗留下大量的垃圾背后没有被清除,而我在Jprofiler中手动进行GC时可以正确进行GC的,所以说问题就很明显了,由于调用该接口后会产生2G多的垃圾,而这些垃圾又没有被及时清除导致了项目占满了服务器的内存。我本地是可以正常运行的,这是因为我本地可以给到项目5G的内存,但是我们公司用于测试的服务器实在是太小了,所以产生的2G多的垃圾堆在服务器上就显得很多了。所以问题就在于这个接口产生的垃圾太多,只要想办法及时清除这些垃圾或者让他不产生这么多垃圾就好了

解决问题

当然我首先想的是优化代码,不要让这份代码在调用之后产生这么多垃圾,然而很不幸的,这份代码是四年前写好的,整份代码可以说是又臭又长,别说优化了,光是理解内部逻辑都让人头昏眼花,如果真要优化,那显然需要评估。但是正所谓程序员最擅长的就是做一堆屎山代码,机智的我突然想到可以使用System.gc()来让系统每次运行完该接口后自动进行GC,这样不就解决这个问题了?而且说到底现在出现这个问题的原因是测试服务器的内存太小,到生产环境这个问题就不存在了,我只要保证测试的时候不出问题就行了,那只要保证测试服务器可以正常运行就可以了,生产上这个问题直接不管他也不会有问题。

说干就干,我立刻在接口上的最后加入了System.gc(),通过查看Jprofiler会发现执行完该接口后是可以正确回收垃圾的,那么说明这样做是可以行的,我即可把它部署到服务器上看看运行效果

新的问题

然而,部署到服务器上却出了问题,尽管在本地上可以调用System.gc()的代码,但是在Linux服务器上却没有看到效果,使用free命令会发现可用的空间仍然不足100M,我首先查看日志看看这份代码有没有被执行,查看了日志发现这个代码是有被正确执行的,代码本身没有问题。那会不会是因为服务器上设置了禁用手动GC的设置呢?因为根据好心论坛的帮助,我了解到System.gc()不起作用很有可能是在参数设置了 disableexplicitgc 导致的,然而确认了启动配置之后我发现显然也没有这样设置,也就是说不知道为什么,system.gc()虽然执行了,但是就是在服务器上没效果。

为了排查这个问题,我只好在服务器上部署Jprofiler,通过远程连接Jprofiler看看在服务器环境里项目的运行环境究竟是怎么样的,这一看不要紧,看完了之后就更奇怪了,因为通过远程连接的Jprofiler,能看到那里显示即使是在远程服务器里也是进行了GC的,也就是说代码本身没有任何问题,GC也没有问题,但是项目占用的内存却并没有减少,这就奇了怪了?按说GC了之后可用空间不应该就会变多了吗?Jprofiler也是这么显示的,那为什么在Linux中的可用空间没有变大呢?

我百度了一下才发现,这原来是因为JVM的垃圾回收,是逻辑上的回收,并不是真正的将内存归还给了操作系统,所以即使进行了GC,free命令查看已使用的内存会显示仍然使用了很多,但这个并不影响我们对操作系统的正常访问。

恍然大悟

虽然解决了上面的问题,但是这样就又出现了一个问题,既然GC没问题,那最开始为什么前端会访问不了接口?认真一想也是啊,GC跟访问接口能有什么关系呢?GC的次数多顶多就是让执行效率降低而已,但是这其实是不应该跟访问不了有什么关系的啊,毕竟跟请求最直接有关系的应该是CPU资源,既然访问不了,那说明大概率是CPU资源出了问题。带着这个想法去查看运行这个接口时的CPU资源图,果然发现这个接口在允许时,使用了多线程,占用了几乎百分百的CPU资源,这就导致其他请求在此时无法被处理。最开始前端出现访问不了接口的问题大概率是因为前端请求项目的时候,CPU的资源都被这个任务占用了,这就导致其他人访问不了,同时由于JVM的GC机制,导致free那边的内存很小,给了我们错误的信息,下意识以为是发生了内存泄漏,其实跟内存泄漏半毛钱关系都没有

最后的解决方法也很简单:

  1. 尽可能不要在测试高峰期请求这个接口,这样就可以避免CPU资源被全部占用的问题。
  2. 优化代码,让这个接口不要占用所有资源,或者令其请求变得更平滑,但这需要评估。
  3. 增大服务器,这个方法最简单,CPU资源不够你就加到他够了就好了,力大砖飞。

问题总结

解决这个问题花了我快一天的时间,由于我之前对GC的知识也只是在八股文阶段,所以这一次的经历让我学习到了很多,总结如下:

  1. 内存只要没有发生OOM,那么就说明GC是正常的,最多就只是效率问题。
  2. 跟无法访问相关的问题,首先可以排查CPU资源的问题。
  3. 远程项目要做Jprofiler监控,便于第一时间监控和解决问题,避免发生问题之后做溯源。