分析日志线程阻塞导致项目假死

本例分析中所用的工具为jvisualvm(以下简称工具),在打开工具之前需要先启动fmm项目。本次排查问题的方式主要是通过获取线程快照,通过实时地查看的方法的出入栈情况,来找到问题的出处。而项目在启动以后,随着出栈与入栈的不断动态变化,每一次线程的静态快照都是不同的,为了更快的定位到问题的出处,我们只能在出问题的那一时段进行快照。换句话说,就是在项目启动以后,我们点击登录页面,项目一直处于等待状态时,我们才能捕捉快照。

具体快照方法如图所示

分析日志线程阻塞导致项目假死_第1张图片

下面生成的就是某一时刻栈方法调用的情况


分析日志线程阻塞导致项目假死_第2张图片

通过分析快照中栈方法的调用情况,很快就能发现有一些线程一直处于阻塞状态。

分析日志线程阻塞导致项目假死_第3张图片

注意那把锁的内存偏移量为0x0000000083a08ca8,顺着这把锁来找,很快就能找到他的出生地


分析日志线程阻塞导致项目假死_第4张图片

根据上图所示再结合log4j的源码就能找到这把锁的出生地


分析日志线程阻塞导致项目假死_第5张图片

为了线程安全,产生锁是一个再正常不过的事情,大不了再释放它就得了。我们再继续追踪这个打印日志的线程,稍微有点眼神的人很快就会发现,这个打印日志的线程在还没有释放掉第一把锁的时候又立马生成了第二把锁,如图


分析日志线程阻塞导致项目假死_第6张图片

而这第二把锁就是我们要找的直接线索。这第二把锁是个什么角色呢?聪明的观众肯定一眼就看到了org.apache.log4j.net.SocketAppender,看到了socket这个字眼,立马我们就会想到它是一个网络通信协议(其实顺着这个线程的方法栈往上找也能证实这个结论)。有观众就会不解,打印日志怎么还扯上远程通信了。在此不得不为elk日志收集插播一段广告啦。elk是一套专门收集各种日志的框架,为了提高它自身的功力,底层支持了各种数据通信协议,其中一种就包括socket。为了远程收集fmm项目的日志,项目也引进了elk。使用方法很简单,你只需要在log4j.properties文件中进行配置即可。


分析日志线程阻塞导致项目假死_第7张图片
分析日志线程阻塞导致项目假死_第8张图片

到此我们就真正追踪到了凶手的作案现场。此时又有观众表示不解,为什么以前没有发生过类似案件,而现在却作案频频呢?带着这个问题,我又查看了远程服务端的elk。


一开始我会很自然地怀疑到logstash身上,因为他是进行日志收集的第一直接者。但是通过端口排查,很快这个结论被否定了。

忽然一下子又没有了头绪,在敲下 jps这个命令后,问题又浮出来了。

分析日志线程阻塞导致项目假死_第9张图片


说好的桃园三结义!怎么只有了两个人了,还一个kafka跑哪儿去了。此时又有人会问。你们不是集群吗,走了一个不是还有另外两个吗?这个就只能怪当初配置elk的作者(也就是本人)偷懒了。在搭建elk的时候,鄙人为了偷一下懒,在配置logstash与kafka的关系时,就只写了一个kafka与logstash发生关系,没有写另外两个(原则上三者都要写上)。而恰恰跑掉的那个kafka就是我配置的kafka。

分析日志线程阻塞导致项目假死_第10张图片
分析日志线程阻塞导致项目假死_第11张图片

到此,真正的凶手才算捉拿归案。真凶就是远隔千里之外的,专门用来缓存日志的kafka,而它却突然跑路了。所以不管我们怎么在fmm的代码层面上花力气,都是吃力不讨好!也解释了为什么之前大家国泰民安,却突然之间世界末日!

为了更加说明问题,让我用一个大胆的比喻来缕一缕这一条线。当项目启动以后,我们还能正常的生出一个“日志宝宝”,根据之前的剧情我们知道,当“线程妈妈”生出这个“日志宝宝”之前,在进入产房(也就是内存中的“栈帧”)之时,“线程妈妈”手上就一直紧握着第一层产房门和第二层产房门的钥匙,而当“线程妈妈”生出了她的“日志宝宝”宝宝以后,当她准备要走出产房把两把钥匙交给后续的“线程妈妈”的时候,却迟迟等不到医生(远程logstash)给她一个明确的回复。。。孩子是死是活???出于“爱子心切”这个“线程妈妈”就一直呆在产房中焦急的等待。而更焦急的画面是,产房外面却早已排满了其他待生产的“线程妈妈”!


分析日志线程阻塞导致项目假死_第12张图片


分析日志线程阻塞导致项目假死_第13张图片


分析日志线程阻塞导致项目假死_第14张图片

通过上面的比喻,我们大致了解了锁等待的原因。最终还是要回到解决问题的层面上。找到了问题,一切也就变的迎刃而解了。以下是几种解决方案。

1. 关掉kafka集群重新打开kafka

2. 注释掉fmm项目中的logstash日志收集

3. 把log4j升级到2.x

4. 把日志打印的级别调到warn级别

5. 日志打印的线程改为异步形式(这种方式没有解决根本问题,只是缓和,因为锁的问题还是存在,只是被异步隐藏了,用户感觉不到而已,随着打印日志新线程不断阻塞与累计,最终会挤爆线程池或还没等线程池挤爆,内存先报了stackoverflow的错误)


有人会问,为什么项目启动的时候还能打印远远不止一条的日志呢?不是说只打印第一条日志,后续的就处于阻塞状态吗?

其实我们只要仔细查看一下线程快照就会明白,产生锁等待的日志打印就是在debug级别发生的,请看下图。


分析日志线程阻塞导致项目假死_第15张图片


分析日志线程阻塞导致项目假死_第16张图片

所以,只要我们避开debug级别的日志打印也可以避开锁等待(本人亲测有效,可惜当时没有现场截图)。但是使用warn或其他级别的日志实现,没有根本解决问题,最根本的还是升级log4j或者干脆阉割掉本项目的elk日志收集,或者重新连接好elk即可。虽然当时没有现场截图warn级别的打印实现。下面却有张图可以旁证。

https://dzone.com/articles/log4j-thread-deadlock-case


分析日志线程阻塞导致项目假死_第17张图片

到此,又有吃瓜的群众表示不解了。为什么把log4j升级到2.x就能解决问题。这个问题,请观众自行百度脑补

你可能感兴趣的:(分析日志线程阻塞导致项目假死)