分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。
分类: 服务器与存储
2020-01-13 11:14:50
本文首发于 vivo互联网技术 微信公众号
链接:
作者:黄卫兵、陈锦霞
某接口/get.do压测,3分钟后,成功事务数TPS由1W骤降至0。
被压测服务器,出现TCP CLOSE_WAIT状态个数在200~2W左右。
通过jstack打印Tomcat堆栈信息,发现“Found 1 deadlock”
内部讨论后,认为当前Tomcat版本可能有Bug。不影响项目进度,简单修改方案把SpringBoot 使用的Tomcat 9.0.26 降级到Tomcat 8。降级后再次压测,没有发现问题。基本上可以确定Tomcat 9.0.26 应该是存在 Deadlock 问题。
为了确认问题,我们试着给Tomcat提交Bug反馈。
从堆栈信息来看,是3类线程5个线程由于加锁的顺序不致,从而相互等待发生了死锁。图形化上面加锁的过程如下图。
明确了死锁的过程,但是哪个环节出了问题呢。这就需要深入到源码层去定位问题。首先需要下载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。两者都需要先锁定,但锁定的顺序不一致,从而导致死锁。
在提交Bug后,很快得到了Remy Maucherat的回复,首先他提到这个NIO内部的死锁。然后我们提到NIO内部的死锁是由于Poller.run和Poller.canceledKey在并发时导到的。
Remy Maucherat很快就进行了修复,主要是把Poller.canceledKey中close移到了finally中去执行,也就是先让Poller.run获得锁。
在得到修复后,我们使用替换后的代码进行了再次压测,死锁问题没有出现了。Remy Maucherat同时提到在最新的OpenJDK中相关问题的修复,但只会出现在jdk 11和14版本。
沟通中的详情见下图。
使用 提供修复后代码,重新打包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版本。