Tomcat一個BUG造成CLOSE_WAIT


   之前應該提過,我們線上架構整體重新架設了,應用層面使用的是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

微信公眾號:

                      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM