TCP 有很多連接狀態,每一個都夠聊十塊錢兒的,比如我們以前討論過 TIME_WAIT 和 FIN_WAIT1,最近時不時聽人提起 CLOSE_WAIT,感覺有必要梳理一下。
所謂 CLOSE_WAIT,借用某位大牛的話來說應該倒過來叫做 WAIT_CLOSE,也就是說「等待關閉」,如果你還不理解其含義,可以看看 TCP 關閉連接時的圖例:
不要被圖中的 client 和 server 所迷惑,你只要記住:主動關閉的一方發出 FIN 包,被動關閉的一方響應 ACK 包,此時,被動關閉的一方就進入了 CLOSE_WAIT 狀態。如果一切正常,稍后被動關閉的一方也會發出 FIN 包,然后遷移到 LAST_ACK 狀態。
通常,CLOSE_WAIT 狀態在服務器停留時間很短,如果你發現大量的 CLOSE_WAIT 狀態,那么就意味着被動關閉的一方沒有及時發出 FIN 包,一般有如下幾種可能:
- 程序問題:如果代碼層面忘記了 close 相應的 socket 連接,那么自然不會發出 FIN 包,從而導致 CLOSE_WAIT 累積;或者代碼不嚴謹,出現死循環之類的問題,導致即便后面寫了 close 也永遠執行不到。
- 響應太慢或者超時設置過小:如果連接雙方不和諧,一方不耐煩直接 timeout,另一方卻還在忙於耗時邏輯,就會導致 close 被延后。響應太慢是首要問題,不過換個角度看,也可能是 timeout 設置過小。
- BACKLOG 太大:此處的 backlog 不是 syn backlog,而是 accept 的 backlog,如果 backlog 太大的話,設想突然遭遇大訪問量的話,即便響應速度不慢,也可能出現來不及消費的情況,導致多余的請求還在隊列里就被對方關閉了。
如果你通過「netstat -ant」或者「ss -ant」命令發現了很多 CLOSE_WAIT 連接,請注意結果中的「Recv-Q」和「Local Address」字段,通常「Recv-Q」會不為空,它表示應用還沒來得及接收數據,而「Local Address」表示哪個地址和端口有問題,我們可以通過「lsof -i:<PORT>」來確認端口對應運行的是什么程序以及它的進程號是多少。
如果是我們自己寫的一些程序,比如用 HttpClient 自定義的蜘蛛,那么八九不離十是程序問題,如果是一些使用廣泛的程序,比如 Tomcat 之類的,那么更可能是響應速度太慢或者 timeout 設置太小或者 BACKLOG 設置過大導致的故障。
此外還有一點需要說明:按照前面圖例所示,當被動關閉的一方處於 CLOSE_WAIT 狀態時,主動關閉的一方處於 FIN_WAIT2 狀態。 那么為什么我們總聽說 CLOSE_WAIT 狀態過多的故障,但是卻相對少聽說 FIN_WAIT2 狀態過多的故障呢?這是因為 Linux 有一個「tcp_fin_timeout」設置,控制了 FIN_WAIT2 的最大生命周期。壞消息是 CLOSE_WAIT 沒有類似的設置,如果不重啟進程,那么 CLOSE_WAIT 狀態很可能會永遠持續下去;好消息是如果 socket 開啟了 keepalive 機制,那么可以通過相應的設置來清理無效連接,不過 keepalive 是治標不治本的方法,還是應該找到問題的症結才對。
寫得都比我好,建議大家仔細閱讀。