原文:https://xie.infoq.cn/article/e82a53c6d69fb9852e68ce8da
作者:linux大學營
CLOSE_WAIT 和 TIME_WAIT 是如何産生的?大量的 CLOSE_WAIT 和 TIME_WAIT 又有何隐患?本文将通過實踐角度來帶你揭開 CLOSE_WAIT 和 TIME_WAIT 的神秘面紗!遇事不決先祭此圖:

稍微補充一點:TIME_WAIT 到 CLOSED,這一步是逾時自動遷移。
兩條豎線分别是表示:
- 主動關閉(active close)的一方
- 被動關閉(passive close)的一方
網絡上類似的圖有很多,但是有的細節不夠,有的存在誤導。有的會把兩條線分别标記成 Client 和 Server。給讀者造成困惑。對于斷開連接配接這件事,用戶端和服務端都能作為主動方發起,也就是「主動關閉」可以是用戶端,可以是服務端。而對端相應的就是「被動關閉」。不管誰發起,狀态遷移如上圖。
1. 耗盡的是誰的端口?
首先解答初學者對 socket 的認識存在一個常見的誤區。作為服務端,不管哪個 WAIT 都不會耗盡用戶端的端口!
舉個例子,我在某雲的雲主機上有個 Server 程式:echo_server。我啟動它,監聽 2605 端口。然後我在自己的 MacBook 上用 telnet 去連接配接它。連上之後,在雲主機上用 netstat -anp 看一下:
[guodong@yun test]netstat -anp|grep 2605
tcp 0 0 0.0.0.0:2605 0.0.0.0:* LISTEN 3354/./echo_server
tcp 0 172.12.0.2:2605 xx.xx.xx.xx:31559 ESTABLISHED 3354/./echo_server
xx.xx.xx.xx 是我 Macbook 的本機 IP
其中有兩條記錄,LISTEN 的表示是我的 echo_server 監聽一個端口。ESTABLISHED 表示已經有一個用戶端連接配接了。第三列的 IP 端口是我 echo_server 的(這個顯示 IP 是區域網路的;第四列顯示的是用戶端的 IP 和端口,也就是我 MacBook。
要說明的是這個端口:31559 是用戶端的。這個是建立連接配接時的 MacBook 配置設定的随機端口。
我們看一下 echo_server 占用的 fd。使用 ls /proc/3354/fd -l 指令檢視,其中 3354 是 echo_server 的 pid:
0 -> /dev/pts/6
1 -> /dev/pts/6
2 -> /dev/pts/6
3 -> anon_inode:[eventpoll]
4 -> socket:[674802865]
5 -> socket:[674804942]
0,1,2 是三巨頭(标準輸入,輸出,錯誤)自不必言。3 是因為我使用了 epoll,是以有一個 epfd。
4 其實就是我服務端監聽端口打開的被動套接字;
5 就是用戶端建立連接配接到時候,配置設定給用戶端的連接配接套接字。server 程式隻要給 5 這個 fd 寫資料,就相當于傳回資料給用戶端。
服務端怎麼會耗盡用戶端的端口号的。這裡消耗的其實是服務端的 fd(也不是端口)!
2. 什麼時候出現 CLOSE_WAIT?
2.1 舉個例子
回到我的 MacBook 終端,檢視一下 2605 有關的連接配接(Mac 上 netstat 不太好用,隻能用 lsof 了):
[guodong@MacBook test]lsof -iTCP:2605
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
telnet 74131 guodong 3u IPv4 0x199db390a76b3eb3 0t0 TCP 192.168.199.155:50307->yy.yy.yy.yy:nsc-posa(ESTABLISHED)
yy.yy.yy.yy 表示的遠端雲主機的 IP
nsc-posa 其實就是端口 2605,因為 2605 也是某個經典協定(NSC POSA)的預設端口,是以這種網絡工具直接顯示成了那個協定的名稱。
用戶端 pid 為 74135。當然,我其實知道我是用 telnet 連接配接的,隻是為了查 pid 的話,ps aux|grep telnet 也可以。
注意:為了測試。我這裡的 echo_server 是寫的有問題的。就是沒有處理用戶端異常斷開的事件。
下面我 kill 掉 telnet(kill -9 74131)。再回到雲主機檢視一下:
[guodong@yun test]netstat -anp|grep 2605
tcp 0 0 0.0.0.0:2605 0.0.0.0:* LISTEN 3354/./echo_server
tcp 1 172.12.0.2:2605 xx.xx.xx.xx:31559 CLOSE_WAIT 3354/./echo_server
由于 echo_server 内沒對連接配接異常進行偵測和處理。是以可以看到原先 ESTABLISHED 的連接配接變成了 CLOSE_WAIT。并且會持續下去。我們再看一下它打開的 fd:
0 -> /dev/pts/6
1 -> /dev/pts/6
2 -> /dev/pts/6
3 -> anon_inode:[eventpoll]
4 -> socket:[674865719]
5 -> socket:[674865835]
5 這個 fd 還存在,并且會一直存在。是以當有大量 CLOSE_WAIT 的時候會占用伺服器的 fd。而一個機器能打開的 fd 數量是有限的。超過了,因為無法配置設定 fd,就無法建立新連接配接啦!
2.2 怎麼避免用戶端異常斷開時的服務端 CLOSE_WAIT?
有一個方法。比如我用了 epoll,那麼我監聽用戶端連接配接套接字(5)的 EPOLLRDHUP 這個事件。當用戶端意外斷開時,這個事件就會被觸發,觸發之後。我們針對性的對這個 fd(5)執行 close()操作就可以了。改下代碼,重新模拟一下上述流程,blabla 細節略過。現在我們新 echo_server 啟動。MacBook 的 telnet 連接配接成功。然後我 kill 掉了 telnet。觀察一下雲主機上的狀況:
[guodong@yun test]netstat -anp|grep 2605
tcp 0 0 0.0.0.0:2605 0.0.0.0:* LISTEN 7678/./echo_server
tcp 1 172.12.0.2:2605 xx.xx.xx.xx:31559 LAST_ACK -
出現了 LAST_ACK。我們看下 fd。指令:ls /proc/7678/fd -l
0 -> /dev/pts/6
1 -> /dev/pts/6
2 -> /dev/pts/6
3 -> anon_inode:[eventpoll]
4 -> socket:[674905737]
fd(5)其實已經關閉了。過一會我們重新 netstat 看下:
[guodong@yun test]netstat -anp|grep 2605
tcp 0 0 0.0.0.0:2605 0.0.0.0:* LISTEN 7678/./echo_server
LAST_ACK 也消失了。為什麼出現 LAST_ACK。翻到開頭,看我那張圖啊!
CLOSE_WAIT 不會自動消失,而 LAST_TACK 會逾時自動消失,時間很短,即使在其存續期内,fd 其實也是關閉狀态。實際我這個簡單的程式,測試的時候不會每次都捕捉到 LAST_WAIT。有時候用 netstat 指令檢視的時候,就是最終那副圖了。
3. 什麼時候出現 TIME_WAIT?
看我開篇那個圖就知道了。
現在我 kill 掉我的 echo_server!
[guodong@yun test]netstat -anp|grep 2605
tcp 0 0 172.17.0.2:2605 xx.xx.xx.xx:51327 TIME_WAIT -
雲主機上原先 ESTABLISHED 的那條瞬間變成 TIME_WAIT 了。
這個 TIME_WAIT 也是逾時自動消失的。時間是 2MSL。MSL 是多長?
cat /proc/sys/net/ipv4/tcp_fin_timeout
一般是 60。2MSL 也就 2 分鐘。在 2 分鐘之内,對服務端有啥副作用嗎?有,但問題不大。那就是這期間重新啟動 Server 會報端口占用。這個等待,一方面是擔心對方收不到自己的确認,等對方重發 FIN。另一方面 2MSL 是封包的最長生命周期,可以避免 Server 重新開機(或其他 Server 綁同樣端口)接收到了上一次的資料。
當然這個 2MLS 的等待,也可以通過給 socket 添加選項(SO_REUSEADDR)的方式來避免。Server 可以立即重新開機(這樣 Server 的監控程序就可以放心的重新拉起 Server 啦)。
通常情況下 TIME_WAIT 對服務端影響有限,而大量 CLOSE_WAIT 風險較高,但正确編寫代碼基本可以避免。為什麼隻說通常情況呢?因為生産環境是複雜的,一個服務通常會和多個下遊服務用各種各樣的協定進行通信。TIME_WAIT 和 CLOSE_WAIT 在一些異常條件下,還是會觸發的。
并不是說 TIME_WAIT 就真的無風險,其實無論是 TIME_WAIT 還是 CLOSE_WAIT,永遠記住當你的服務出現這兩種現象的時候,它們隻是某個問題導緻的結果,而不是問題本身。有些網絡教程教你怎麼調大這個或那個的 OS 系統設定,個人感覺隻是治标不治本。找到本質原因,避免 TIME_WAIT 和 CLOSE_WAIT 的産生,才是問題解決之道!
把這些了解清楚時候,是不是可以輕松應對什麼 4 次揮手之類的面試題了?