之前應該提過,我們線上架構整體重新架設了,應用層面使用的是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方法,而原本的判斷方法是無法做到的,於是問題解決了。至於這段代碼的位置,我在開始就說了,嘿嘿。。。,如果有我看漏的地方,大家務必告訴我。
==========================================================
咱最近用的github:https://github.com/saaavsaaa
微信公眾號: