当前位置: 首页 > Linux

开发造成的内存泄漏问题,本次调查不怪罪!

时间:2023-04-06 12:05:51 Linux

作者:枕边书来源:https://zhenbianshu.github.io前几天,团队安排值班轮流照顾我们的服务,主要是做一些告警邮件处理,bug排查,运营issue加工。工作日还好,不管做什么,都得上班。如果是周末,那么这一天就毁了。不知道是因为公司网络不够广,还是网络运维团队不够强大。网络总是有问题。要么是这里的交换机断网了,要么是那边的路由器坏了。服务总能准确捕捉到偶尔出现的小问题,为精彩的作品增添一些情趣。好几次,值班组的成员一起吐槽,商量如何避开服务保活机制,偷偷停止检测服务不被发现(虽然不敢)。前几天周末处理一个检测服务锅。本文会持续改版,大家可以持续关注。1.问题网络问题?晚上7点以后,开始不停的收到告警邮件,邮件显示检测到的几个接口都超时了。大多数执行堆栈是:我在这个线程堆栈中看到了很多错误。我们设置的HTTPDNS超时为1s,连接超时为2s,读取超时为3s。收到请求并正常处理后,正常响应,但是数据包在网络层的转发中丢失了,所以请求线程的执行栈会停留在获取接口响应的地方。这种情况的一个典型特征是可以在服务器上找到相应的日志记录。并且日志将显示服务器响应非常好。与之相对的是,线程栈停留在Socketconnect处,连接建立时就失败了,服务端完全没有察觉。我注意到其中一个接口更频繁地报告错误。这个接口需要上传一个4M的文件到服务器,然后经过一系列的业务逻辑处理,然后返回2M的文本数据,而其他接口都是简单的业务逻辑,我猜可能是需要的数据太多了被上传和下载,所以超时导致丢包的概率也更大。根据这个推测,该团伙??登录服务器,使用请求的request_id在最近的服务日志中查找。果然是网络丢包问题导致的接口超时。这样领导当然不会满意,这个结论还得有人接手。于是赶紧联系运维和网络团队,确认了当时的网络状况。网络组同学回复说我们检测服务所在机房的交换机比较老旧,存在未知的转发瓶颈,正在优化中。问题爆发的时候,本以为这个班次会有这么小的波澜,没想到晚上八点多,各个接口的告警邮件蜂拥而至,准备收拾行李的我猝不及防周日休息的事情。这次几乎所有的接口都超时了,但是我们这个网络I/O量大的接口每次检测都要超时。是不是整个机房都出问题了?再次通过服务器和监控看到,各个接口的指标都是正常的。我测试了接口,完全OK。由于不影响在线服务,我打算通过检测服务的接口停止检测任务,然后慢慢查看。结果我向接口发送了暂停检测任务的请求,很久没有响应。我这才知道,事情并没有那么简单。2.解决内存泄露于是赶紧登录检测服务器,首先topfreedf3次,结果果然发现了一些异常。我们检测过程的CPU使用率特别高,达到了900%。我们的Java进程不会做大量的CPU计算。正常情况下,CPU占用率应该在100%到200%之间。如果这种CPU飙升,要么是死循环,要么是在做大量的GC。使用jstat-gcpid[interval]命令查看java进程的GC状态。果然FULLGC达到了每秒一次。这么多FULLGC,应该是内存泄漏没有运行,所以使用jstackpid>jstack.log保存线程堆栈的场景,使用jmap-dump:format=b,file=heap.logpid保存堆场景,然后重启检测服务被禁用,报警邮件终于停止了。jstatjstat是一个非常强大的JVM监控工具,一般用法为:jstat[-options]pidinterval支持查看项目:classview类加载信息compile编译统计信息gc垃圾收集信息gcXXX各个区域GC的详细信息,比如as-gcold就是使用它,这对定位JVM内存问题很有帮助。3、排查问题虽然已经解决,但是为了防止再次发生,还是要找出根本原因。栈的分析栈的分析很简单。查看是否有太多线程以及大多数堆栈在做什么。只有400多个线程,没有异常。线程状态貌似正常,再分析堆文件。下载heapdump文件heap文件都是二进制数据,在命令行查看非常麻烦。Java提供的工具都是可视化的,无法在Linux服务器上查看,所以必须先将文件下载到本地。由于我们设置的堆内存是4G,所以dump出来的堆文件也很大。下载下来确实很麻烦,不过我们可以先压缩一下。gzip是一个非常强大的压缩命令。具体来说,我们可以设置-1~-9来指定它的压缩级别。数据越大,压缩比越大,耗时越长。推荐使用-6~7,-9真的太慢了??,好处也不大。有了这个压缩时间,就会下载额外的文件。使用MAT分析jvmheapMAT是分析Java堆内存的强大工具。用它打开我们的堆文件(将文件后缀改为.hprof),它会提示我们分析的类型。本次分析,果断选择内存泄漏嫌疑。从上面的饼图可以看出,大部分堆内存都被同一个内存占用了。然后查看堆内存的细节,向上层追溯,很快就找到了罪魁祸首。分析代码找到内存泄漏的对象,在项目中全局查找对象名,是一个Bean对象,然后定位到它的一个Map类型的属性。这个Map使用ArrayList按照类型存储各个检测接口的响应结果。每次检测后,塞入ArrayList中进行分析。由于Bean对象不会被回收,而且这个属性也没有清除逻辑,所以十多天没有使用了。在上线重启的情况下,Map会越来越大,直到占满内存。内存满后,无法再为HTTP响应结果分配内存,所以一直卡在readLine。而我们这个I/O比较多的接口,告警次数特别多,估计跟响应大需要更多的内存有关。向代码所有者提了一个PR,问题得到了圆满解决。4.总结其实还是要反省一下自己。最开始在告警邮件中有这样一个线程栈:看到了这种错误线程栈但是没想到。要知道TCP可以保证报文的完整性,报文是收不到的。它根本不会将值分配给变量。这显然是一个内部错误。如果你注意并仔细检查,你可以提前发现问题。如果问题真的很严重,则任何链接都不起作用。