来源:zhenbianshu.github.io/前几天,团队安排值班轮流照顾我们的服务,主要是做一些告警邮件处理,bug排查,运营问题处理。工作日还好,不管做什么,都得上班。如果是周末,那么这一天就毁了。不知道是因为公司网络不够广,还是网络运维团队不够强大。网络总是有问题。要么这里的交换机断开,要么那里的路由器坏了。偶尔也会出现各种超时,我们对服务检测服务比较敏感。总是能够准确把握偶尔出现的小问题,为精彩的作品添砖加瓦。好几次,值班组的成员一起吐槽,商量如何避开服务保活机制,偷偷停止检测服务不被发现(虽然不敢)。前几天周末处理一个检测服务锅。问题网络问题?晚上7点以后,开始不停的收到告警邮件,邮件显示检测到的几个接口都超时了。大多数执行堆栈位于:java.io.BufferedReader.readLine(BufferedReader.java:371)java.io.BufferedReader.readLine(BufferReader.java:389)java_io_BufferedReader$readLine.call(UnknownSource)com.domain.detect.http.HttpClient.getResponse(HttpClient.groovy:122)com.domain.detect.http.HttpClient.this$2$getResponse(HttpClient.groovy)我在这个线程堆栈中看到了很多错误。我们设置的HTTPDNS超时为1s,connect超时为2s,read超时为3s。这种报错是因为检测服务正常发送HTTP请求,服务端收到请求后也正常响应并正常处理,但是数据包在网络层的转发中丢失了,所以请求线程的执行stack会停留在获取接口响应的地方。这种情况的一个典型特征是可以在服务器上找到相应的日志记录。并且日志将显示服务器响应非常好。与之相对的是,线程栈停留在Socketconnect处,连接建立时就失败了,服务端完全没有察觉。我注意到其中一个接口更频繁地报告错误。这个接口需要上传一个4M的文件到服务器,然后经过一系列的业务逻辑处理,返回2M的文本数据,而其他接口都是简单的业务逻辑,我猜测可能是需要上传的数据太多并下载,所以超时导致丢包的概率也更大。据此推测,该团伙??登录服务器,使用请求的request_id在最近的服务日志中查找。不出所料,由于网络丢包导致接口超时。这样领导当然不会满意,这个结论还得有人接手。于是赶紧联系运维和网络团队,确认了当时的网络状况。网络组同学回复说我们检测服务所在机房的交换机比较老旧,存在未知的转发瓶颈,正在优化中。问题爆发的时候,本以为这个班次会有这么小的波澜,没想到晚上八点多,各个接口的告警邮件蜂拥而至,准备收拾行李的我猝不及防周日休息的事情。这个时候几乎所有的接口都超时了,而我们这个网络I/O量大的接口每次检测都要超时。难道是整个机房都出了问题?再次通过服务器和监控看到各个接口的指标都是正常的。我测试了接口,完全OK。由于不影响在线服务,所以我打算先通过检测服务的接口停止检测任务,再慢慢查看。结果我向接口发送了暂停检测任务的请求,很久没有响应。我这才知道,事情并没有那么简单。为了解决内存泄露问题,我赶紧登录了检测服务器。首先,我做了3次topfreedf,发现有些异常。我们的探测过程的CPU使用率特别高,高达900%。另外,JVM系列面试题和答案都整理好了。微信搜索Java技术栈,后台发:面试,可以在线阅读。我们的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支持查看项目:-class查看类加载信息-compile编译统计-gc垃圾回收信息-gcXXX各区GC详细信息等as-gcold就是使用它,这对定位JVM内存问题很有帮助。排查问题虽然解决了,但是为了防止再次发生,还是要找出根本原因。栈的分析栈的分析很简单。查看是否有太多线程以及大多数堆栈在做什么。>grep'java.lang.Thread.State'jstack.log|wc-l>464只有400多个线程,没有异常。>grep-A1'java.lang.Thread.State'jstack.log|grep-v'java.lang.Thread.State'|排序|uniq-c|sort-n10atjava.lang.Class.forName0(NativeMethod)10atjava.郎。目的。等待(本机方法)16在java。郎。类加载器。loadClass(ClassLoader.java:404)44在太阳。.misc.Unsafe.park(NativeMethod)线程状态貌似正常,再分析堆文件。下载堆转储文件。堆文件都是二进制数据,在命令行查看非常麻烦。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,问题得到了圆满解决。总结其实还是要反省自己。一开始在报警邮件中有这样一个线程栈:groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParserCharArray.java:166)groovy.json.internal.JsonParserCharArray.decodeJsonObject(JsonParserCharArray.java:132)groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParserCharArray.java:186)groovy.json.internal.JsonParserCharArray.decodeJsonObject(JsonParserCharArray.java:132)groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParser1CharArray.java:)看到了这种错误线程堆栈但没有仔细考虑。要知道TCP可以保证报文的完整性,直到收到报文才会给变量赋值。这显然是一个内部错误。如果你注意详细调查可以提前发现问题。如果问题真的很严重,则任何链接都不起作用。近期热点文章推荐:1.1000+Java面试题及答案(2022最新版)2.厉害了!Java协程来了。..3.SpringBoot2.x教程,太全面了!4.不要用爆破爆满画面,试试装饰者模式,这才是优雅的方式!!5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!
