之前应该提过,我们线上架构整体重新架设了,应用层面使用的是Spring Boot,前段日子因为一些第三方的原因,略有些匆忙的提前开始线上的内测了。然后运维发现了个问题,服务器的HTTPS端口有大量的CLOSE_WAIT:
我的第一反应是Spring boot有Bug,因为这个项目分为HTTP和HTTPS两种服务以JAR的形式启动的,而HTTP的没有问题,同时,老架构的服务在Tomcat中以HTTPS提供服务也没有问题,我当时认为这大致上可以判断为Socket层面应该是没有问题的,于是我开始分析Spring Boot的代码。
经过调试和分析(过程如果有机会,再整理一篇),虽然没有找到引起这个现象的原因,但是发现一个规律,所有出现问题的连接org.apache.tomcat.util.net.NioEndpoint的内部类SocketProcessor中doRun方法中,握手状态一直处于handshake == SelectionKey.OP_READ,监听一直不会关闭。
虽然,到这一步看上去问题应该出现在Socket层面,但是我还是觉得应该是Spring Boot的,因为Spring Boot引用的Tomcat的处理这部分功能的代码虽然是内嵌的(tomcat-embed-core-8.5.4),但是和完整版并没有什么区别,而完整版是没有这个问题的。
然后,因为两个原因,我决定继续排查,直接去提ISSUE了:一、需要大量时间分析相关代码才能保证解决这个问题不出现其他问题;二、可以肯定这不是我们新架构和开发的问题。于是我去github提了个Issue,问题在:https://github.com/spring-projects/spring-boot/issues/7780,然而第二天果不其然的被建议让我去给Tomcat提Issue:
虽然我依然认为这是在甩锅,但是我并没有什么能证明这不是Tomcat问题的证据。于是我又看了看代码,试图证明一下 ,然而并没有找到。
终于,我去给Tomcat提了个Bug,https://bz.apache.org/bugzilla/show_bug.cgi?id=60555,回复指向了另外一个BUG,是这个版本确实存在这个问题,原因是:
The problem occurs for TLS connections when the connection is dropped after the socket has been accepted but before the handshake is complete. The socket ended up in a loop:
- timeout -> ERROR event
- process ERROR (this is the new bit from r1746551)
- try to finish handshake
- need more data from client
- register with poller for READ
- wait for timeout
- timeout ...
... and around you go.
好吧,既然Tomcat接盘了,咱也不多说啥了,但是我对比了一下本地的类包的代码和r1746551的代码,并且调试了一下以后,发现并不是他说的代码造成的,因为我调试了r1746551的代码依然没有解决问题。不过,线上环境的问题倒是有了个勉强可以接受的解决办法,内嵌的Tomcat换成内嵌的Jetty,果然是没有问题了。
现在gradle.build中排除spring-boot-starter-web对内嵌Tomcat的引用:
compile('org.springframework.boot:spring-boot-starter-web:1.4.0.RELEASE'){
exclude module: "spring-boot-starter-tomcat"
}
然后换成Jetty
[group: 'org.springframework.boot', name: 'spring-boot-starter-jetty', version: '1.4.0.RELEASE'],
至于,提给Tomcat的那个问题,我抽空再仔细琢磨琢磨在去接着提,不过刚才测试升级了一下版本果然是没问题了。
调试了一下,果然感觉解决问题的并不是他写的r1746551,下面是我看代码的时候发现的,直接解决问题的部分,并不包含在r1746551中,原来有问题的部分:
if (socket.isHandshakeComplete() || event == SocketEvent.STOP) {
handshake = 0;
} else {
handshake = socket.handshake(key.isReadable(), key.isWritable());
// The handshake process reads/writes from/to the
// socket. status may therefore be OPEN_WRITE once
// the handshake completes. However, the handshake
// happens when the socket is opened so the status
// must always be OPEN_READ after it completes. It
// is OK to always set this as it is only used if
// the handshake completes.
event = SocketEvent.OPEN_READ;
}
现在没问题的代码是:
if (socket.isHandshakeComplete()) {
// No TLS handshaking required. Let the handler
// process this socket / event combination.
handshake = 0;
} else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT ||
event == SocketEvent.ERROR) {
// Unable to complete the TLS handshake. Treat it as
// if the handshake failed.
handshake = -1;
} else {
handshake = socket.handshake(key.isReadable(), key.isWritable());
// The handshake process reads/writes from/to the
// socket. status may therefore be OPEN_WRITE once
// the handshake completes. However, the handshake
// happens when the socket is opened so the status
// must always be OPEN_READ after it completes. It
// is OK to always set this as it is only used if
// the handshake completes.
event = SocketEvent.OPEN_READ;
}
因为问题本就是因为握手正常建立的过程中被关闭造成的,只要判断改成如上,当握手是由于socket建立失败造成的就会走到close方法,而原本的判断方法是无法做到的,于是问题解决了。至于这段代码的位置,我在开始就说了,嘿嘿。。。,如果有我看漏的地方,大家务必告诉我。