工单处理之--一次CPU满负载的问题排查分享(存在死循环逻辑导致)

一、前言

近期处理过几个生产环境cpu占用率异常的工单了,但迫于种种原因一直没能系统的梳理一下排查过程,正好今天又遇到了一个现场问题,所以留存了一下过程数据,在这里跟大家分享一下。一个是加深一下自己的印象,再一个也给后续处理类似问题的小伙伴提供一些参考资料。期待拍砖!

二、过程

2.1、确认问题

查看工单描述,反馈新版本有问题,还原回老版本就没问题。且发现工单附件已经包含【cpu死锁排查过程.docx】,遂下载查看(有的时候现场给的附件等信息真的对排查问题很有帮助,小伙伴们在处理过程中一定要记得查看哦)

2.2、初步定位

根据附件文件内容确认排查过程没问题(手工点赞@徐海明),但对ftp服务相关代码出现死锁的结论持怀疑态度(根据文档中信息来看,最耗cpu的确实是与ftp关键字有关的线程逻辑处理,但ftp出问题也应该是IO或网络出问题的概率比较大呀,看了下代码也不涉及具体的FTP处理,所以持怀疑态度)

下图是其中占用CPU最高的线程堆栈信息


大概看了下,也没发现什么问题,都是普通的代码,就是有个while循环,平常比较常见的是if判断(难道是因为路径中可能包含多个斜杠?)

2.3、重新定位

既然已有信息看不出来有什么问题,有没有可能现场在排查过程中遗漏了什么信息导致的呢?亲自连接到生产环境,可以看到jvm确实是最耗费CPU的那个进程,进程ID为108516(截图中忘记调整列宽了,将就看吧)

使用Process Explorer工具查看具体线程的CPU占用(windows可使用该工具,linux可以使用PS命令),打开工具后可使用PID列的进程ID匹配,也可以直接使用CPU占用情况排序定位占用较高进程,下图中CPU占用最高的就是我们的108516

右键进程并点击属性(底部的Properties),查看线程详情。在默认CPU排序中可以看到占用CPU最高的线程ID是103944、106664、720等前几个

将十进制的线程ID转换为十六进制的(jvm的线程栈信息里面都是十六进制的,所以我们需要转换一下)

使用jstack命令导出当前jvm线程栈信息(也可以用jvisualvm.exe等工具查看)

然后在线程栈信息里面搜索我们转换后的十六进制线程ID,可以看到跟工单附件【cpu死锁排查过程.docx】中定位的问题代码位置并无不同


连续查看排名前三的线程栈信息,都是相同的代码


PS:线程池的命名记得要规范哦,不然默认线程池名称看不出来是什么业务相关的线程哦

既然二次确认还是这段代码有问题,但在源码里面也没看到有什么明显的问题(现在看来还是太年轻呀,细心点儿是可以发现问题的),而且现场反馈老版本好用,新版本不好用,那就看一下git提交记录吧
哎呦,好像发现了什么了不得的事情!死循环?死循环可不就会导致CPU飙升嘛(但是吧,问题修改者答复当时是为了解决另一个接口获取不到数据的问题才修改的这块逻辑,没有观察当时的CPU使用情况…我想说,应该只是我们没有发现而已…歪打正着了)

再次查看代码修改位置,原while(path.startsWith("/")){path = path.substring(path.indexOf("/"));}代码的indexOf后面增加了个+1(就少了这么两个字符,导致了现场CPU满负荷运行,细节决定成败的典范!!!),书写Test类验证路径开头为/时,原逻辑确实会造成死循环

但是吧,现场反馈有几个其他地区也升级了相同的版本,没问题呢!然后就查询现场数据库确认生产库中存在部分协议中路径属性为/开头的记录(转义后为%2F)
同时跟问题修改人确认,该缺陷已经合并到最新版代码中。待现场用户下班后,现场升级新版本,经验证问题未再次复现

PS:经过小伙伴提醒,while循环可以使用org.apache.commons.lang3.StringUtils#stripStart解决


import org.apache.commons.lang3.StringUtils;

public class Test {

	public static void main(String[] args) {
		System.out.println(StringUtils.stripStart("abc/def", "/")); // 输出结果:abc/def
		System.out.println(StringUtils.stripStart("/abc/def", "/")); // 输出结果:abc/def
		System.out.println(StringUtils.stripStart("////abc/def", "/")); // 输出结果:abc/def
	}

}

三、结语

1、细节决定成败,我们的代码一定要经过最低自己充分的测试。上面的逻辑中,如果书写完毕后,随便找一条符合条件的数据测试,都能发现该问题,可能就是由于一时的粗心导致线上崩溃(而且这个代码的上次提交日期是17年12月,这个问题竟然潜伏了这么久没有被发现…)
2、善于用轮子或者造轮子(这里完全可以封装成一个通用的方法或使用StringUtils.removeStart来实现–如果不考虑路径中包含多个斜杠的话),通用方法出问题的概率一般情况下比较低
3、要善于总结经验并共享给大家,避免重复踩坑,形成正向循环

猜你喜欢

转载自blog.csdn.net/leandzgc/article/details/106225745