Tomcat 9.0.26 高并发场景下DeadLock问题排查与修复

本文首发于 vivo互联网技术 微信公众号 
连接: https://mp.weixin.qq.com/s/-OcCDI4L5GR8vVXSYhXJ7w
做者:黄卫兵、陈锦霞

1、Tomcat容器 9.0.26 版本 Deadlock 问题

1.1 问题现象

1.1.1  发生 Deadlock 的背景

某接口/get.do压测,3分钟后,成功事务数TPS由1W骤降至0。java

1.1.2  Tomcat服务器出现大量的CLOSE_WAIT

被压测服务器,出现TCP CLOSE_WAIT状态个数在200~2W左右。git

1.2 初步定位:线程堆栈信息入手

经过jstack打印Tomcat堆栈信息,发现“Found 1 deadlock”github

Found one Java-level deadlock:
=============================
"http-nio-8080-exec-409":
waiting to lock monitor 0x00007f064805aa78 (object 0x00000006c0ebf148, a java.util.HashSet),
which is held by "http-nio-8080-ClientPoller"
"http-nio-8080-ClientPoller":
waiting to lock monitor 0x00007f05e8061058 (object 0x00000007bfe40a70, a java.lang.Object),
which is held by "http-nio-8080-exec-205"
"http-nio-8080-exec-205":
waiting to lock monitor 0x00007f0614018448 (object 0x00000006c0e8e088, a java.util.HashSet),
which is held by "http-nio-8080-BlockPoller"
"http-nio-8080-BlockPoller":
waiting to lock monitor 0x0000000001ed06e8 (object 0x00000007bfe110f8, a java.lang.Object),
which is held by "http-nio-8080-exec-380"
"http-nio-8080-exec-380":
waiting to lock monitor 0x00007f064805aa78 (object 0x00000006c0ebf148, a java.util.HashSet),
which is held by "http-nio-8080-ClientPoller"

1.2.1  快速修复方案

内部讨论后,认为当前Tomcat版本可能有Bug。不影响项目进度,简单修改方案把SpringBoot 使用的Tomcat 9.0.26 降级到Tomcat 8。降级后再次压测,没有发现问题。基本上能够肯定Tomcat 9.0.26 应该是存在 Deadlock 问题。apache

1.3  问题进一步跟踪

1.3.1  向Apache社区的反馈

为了确认问题,咱们试着给Tomcat提交Bug反馈。tomcat

从堆栈信息来看,是3类线程5个线程因为加锁的顺序不致,从而相互等待发生了死锁。图形化上面加锁的过程以下图。服务器

1.4 问题缘由分析

明确了死锁的过程,可是哪一个环节出了问题呢。这就须要深刻到源码层去定位问题。首先须要下载OpenJDK 源码,而后是Tomcat 9.0.26 的源码。根据堆栈信息,定位到相应的代码位置。咱们理出以下图Tomcat 9.0.26死锁流程说明。微信

要比较好的理解上图,须要对于NIO有必定的了解。在Tomcat中NIO主要是理解NIO Endpoint。并发

Poller是对于Selector的一个封装,而线程名为exec-xx的执行线程是Channel的封装。在NIO中Channel注册到Selector而后经过SelectionKey来记录对应关系。到此,主角都上场了。高并发

Poller的run方法做为后台线程一直在轮询(select)准备好的SelectionKey,在轮询的时候也顺便须要把cancelledKey中的SelectionKey给反注册。执行线程EXEC-XX在处理时会先判断链接的状态,好比失败、异常等状况会调用Channel的close方法去关闭链接。阿里云

而Channel的close实际只是把SelectionKey加入到cancelledKey。二者都须要先锁定,但锁定的顺序不一致,从而致使死锁。

1.4.1  与Tomcat开发者的交流

在提交Bug后,很快获得了Remy Maucherat的回复,首先他提到这个NIO内部的死锁。而后咱们提到NIO内部的死锁是因为Poller.run和Poller.canceledKey在并发时导到的。

Remy Maucherat很快就进行了修复,主要是把Poller.canceledKey中close移到了finally中去执行,也就是先让Poller.run得到锁。

在获得修复后,咱们使用替换后的代码进行了再次压测,死锁问题没有出现了。Remy Maucherat同时提到在最新的OpenJDK中相关问题的修复,但只会出如今jdk 11和14版本。

沟通中的详情见下图。

1.4.2  Github上修复的验证

https://github.com/apache/tomcat/commit/9b1a8b67bffe462fc745b19e15ed59c37e2e1dcf

1.5 结果验证

使用 https://github.com/apache/tomcat/commit/9b1a8b67bffe462fc745b19e15ed59c37e2e1dcf 提供修复后代码,从新打包tomcat-embed-core.jar 替换9.X.XX的再次压测,TPS平稳在1.5W左右。

到此问题基本是定位清楚,并获得了修复。Remy Maucherat也回复到“The fix will be in Tomcat 9.0.31+”。

目前Tomcat 最新版本是Tomcat 9.0.30,还须要耐心等待31版本更新。建议使用Tomcat 8版本。

2、相关连接与参考

  1. OpenJdk源码下载
  2. Tomcat 源码
  3. 来自阿里云溪社区:断网故障时Mtop触发Tomcat高并发场景下的BUG排查和修复
  4. 深度解读Tomcat中的NIO模型 

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:labs2020 联系。

相关文章
相关标签/搜索