TCP連線狀態異常記錄

aoho發表於2018-07-26

問題描述

分散式事務Lottor在測試環境中執行一段時間之後,出現Lottor客戶端連線不上Lottor Server的情況。經過排查,發現根源問題是Lottor客戶端獲取不到Lottor Server的叢集資訊。

Lottor Server啟動了兩個埠:9666為Tomcat容器的埠、9888為netty 伺服器的埠。通過如下命令檢視埠的狀態:

netstat -apn
複製程式碼

netty服務的埠

netstat -apn|grep 9888
複製程式碼

首先檢視了netty伺服器的埠,連線很正常,和我們上面描述的問題沒什麼聯絡。排除netty服務連線的因素。

Tomcat容器的埠

其次檢視Tomcat容器的埠的狀態:

netstat -apn|grep 9666
複製程式碼

發現有大量的FIN_WAIT2 和 CLOSE_WAIT狀態的連線。

image

另外還可以通過如下的命令檢視當前的連線數:

netstat -apn|grep 9666 | wc -l
複製程式碼

更加細化可以發現FIN_WAIT2 和 CLOSE_WAIT狀態的連線數量相當。

TCP連線狀態異常記錄

TCP四次揮手

通訊的客戶端和服務端通過三次握手建立TCP連線。當資料傳輸完畢後,雙方都可釋放連線。最開始的時候,客戶端和伺服器都是處於ESTABLISHED狀態,然後客戶端主動關閉,伺服器被動關閉。

image

連線的主動斷開是可以發生在客戶端,也同樣可以發生在服務端。常用的三個狀態是:ESTABLISHED 表示正在通訊,TIME_WAIT 表示主動關閉,CLOSE_WAIT 表示被動關閉。

ESTABLISHED

最開始的時候,客戶端和伺服器都是處於ESTABLISHED狀態,然後客戶端或服務端發起主動關閉,另一端則是被動關閉。

FIN_WAIT1

當一方接受到來自應用斷開連線的訊號時候,就傳送 FIN 資料包來進行主動斷開,並且該連線進入 FIN_WAIT1 狀態,連線處於半段開狀態(可以接受、應答資料,當不能傳送資料),並將連線的控制權託管給 Kernel,程式就不再進行處理。一般情況下,連線處理 FIN_WAIT1 的狀態只是持續很短的一段時間。

CLOSE_WAIT

被動關閉的一端在收到FIN包文之後,其所處的狀態為CLOSE_WAIT,表示被動關閉。

FIN_WAIT2

當主動斷開一端的 FIN 請求傳送出去後,並且成功夠接受到相應的 ACK 請求後,就進入了 FIN_WAIT2 狀態。其實 FIN_WAIT1 和 FIN_WAIT2 狀態都是在等待對方的 FIN 資料包。當 TCP 一直保持這個狀態的時候,對方就有可能永遠都不斷開連線,導致該連線一直保持著。

TIME_WAIT

從以上TCP連線關閉的狀態轉換圖可以看出,主動關閉的一方在傳送完對對方FIN報文的確認(ACK)報文後,會進入TIME_WAIT狀態。TIME_WAIT狀態也稱為2MSL狀態。MSL值得是資料包在網路中的最大生存時間。產生這種結果使得這個TCP連線在2MSL連線等待期間,定義這個連線的四元組(客戶端IP地址和埠,服務端IP地址和埠號)不能被使用。

原因分析

在上一小節介紹了TCP四次揮手的相關狀態之後,我們將會分析在什麼情況下,連線處於CLOSE_WAIT狀態呢? 在被動關閉連線情況下,已經接收到FIN,但是還沒有傳送自己的FIN的時刻,連線處於CLOSE_WAIT狀態。 通常來講,CLOSE_WAIT狀態的持續時間應該很短,正如SYN_RCVD狀態。但是在一些特殊情況下,就會出現連線長時間處於CLOSE_WAIT狀態的情況。

出現大量close_wait的現象,主要原因是某種情況下對方關閉了socket連結,但是另一端由於正在讀寫,沒有關閉連線。程式碼需要判斷socket,一旦讀到0,斷開連線,read返回負,檢查一下errno,如果不是AGAIN,就斷開連線。

Linux分配給一個使用者的檔案控制程式碼是有限的,而TIME_WAIT和CLOSE_WAIT兩種狀態如果一直被保持,那麼意味著對應數目的通道就一直被佔著,一旦達到控制程式碼數上限,新的請求就無法被處理了,接著就是大量Too Many Open Files異常,導致tomcat崩潰。關於TIME_WAIT過多的解決方案參見TIME_WAIT數量太多

常見錯誤原因

從原理上來講,由於Server的Socket在客戶端已經關閉時而沒有呼叫關閉,造成伺服器端的連線處在“掛起”狀態,而客戶端則處在等待應答的狀態上。此問題的典型特徵是:一端處於FIN_WAIT2 ,而另一端處於CLOSE_WAIT。具體來說:

1.程式碼層面上未對連線進行關閉,比如關閉程式碼未寫在 finally 塊關閉,如果程式中發生異常就會跳過關閉程式碼,自然未發出指令關閉,連線一直由程式託管,核心也無權處理,自然不會發出 FIN 請求,導致連線一直在 CLOSE_WAIT 。

2.程式響應過慢,比如雙方進行通訊,當客戶端請求服務端遲遲得不到響應,就斷開連線,重新發起請求,導致服務端一直忙於業務處理,沒空去關閉連線。這種情況也會導致這個問題。

Lottor中的問題

Lottor中,客戶端定時重新整理本地儲存的Lottor Server的地址資訊,具體來說是每60秒重新整理一次,通過HttpClient請求獲取結果。筆者根據網上查詢的資料和如上的分析,首先嚐試將週期性重新整理關閉,觀察Lottor Server的9666埠的連線數。之前線性增長的連線數停了下來,初步定位到問題的根源。

伺服器A是一臺爬蟲伺服器,它使用簡單的HttpClient去請求資源伺服器B上面的apache獲取檔案資源,正常情況下,如果請求成功,那麼在抓取完 資源後,伺服器A會主動發出關閉連線的請求,這個時候就是主動關閉連線,伺服器A的連線狀態我們可以看到是TIME_WAIT。如果一旦發生異常呢?假設 請求的資源伺服器B上並不存在,那麼這個時候就會由伺服器B發出關閉連線的請求,伺服器A就是被動的關閉了連線,如果伺服器A被動關閉連線之後程式設計師忘了 讓HttpClient釋放連線,那就會造成CLOSE_WAIT的狀態了。

筆者的場景如上面的情況一般,該問題的解決方法是我們在遇到異常的時候應該直接呼叫中止本次連線,以防CLOSE_WAIT狀態的持續。案例可以參見HttpClient連線池丟擲大量ConnectionPoolTimeoutException: Timeout waiting for connection異常排查。筆者後來的改進方法是重寫了這部分的邏輯,由Spring Cloud FeignClient呼叫執行,避免之前的負載均衡查詢等繁瑣的操作。

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

  1. HttpClient連線池丟擲大量ConnectionPoolTimeoutException: Timeout waiting for connection異常排查
  2. 網路連線無法釋放—— CLOSE_WAIT
  3. TCP三次握手四次揮手詳解

相關文章