<div style="text-align: right">LexusLee</div>
背景
最近踩到一個 "Socket 連接持續處于 Fin_Wait2 和 Close_Wait 狀態無法關閉" 的坑中。起因是在維護大量連接時調用 socket.close()
時,看到部分連接并沒有正常關閉,而是從 ESTABLISHED
的狀態變成 FIN_WAIT2
并且連接狀態沒有后續遷移,而對端的連接狀態則是從 ESTABLISHED
變成了 CLOSE_WAIT
。
后來發現這和 TCP/IP 棧的4次揮手斷開連接有關,列出一些踩坑時的收獲。
Socket 連接關閉的流程
先看一張 Socket 關閉連接的狀態遷移路徑圖:
在 Client 端調用 socket.close()
時,首先會往對端(即 Server 端)發送一個 FIN 包,接著將自身的狀態置為 FIN_WAIT1
,此時主動關閉端(即 Client 端)處于持續等待接收對端的響應 FIN 包的 ACK 回應狀態,此時對端的狀態是處于 ESTABLISHED
,一旦收到了 Client 發來的 close 連接請求,就回應一個 FIN 包,表示收到該請求了,并將自身狀態置為 CLOSE_WAIT
,這時開始等待 Server 端的應用層向 Client 端發起 close 請求。
這時 Client 端一旦收到 Server 端對第一個 FIN 包的回應 ACK 就會將進入下一個狀態 FIN_WAIT_2
來等待 Server 發起斷開連接的 FIN 包。在FIN_WAIT_1 的 time_wait 中, Server 端會發起 close 請求,向 Client 端發送 FIN 包,并將自身狀態從 CLOSE_WAIT
置為 LAST_ACK
,表示 Server 端的連接資源開始釋放了。同時 Client 端正處于 FIN_WAIT2
狀態,一旦接收到 Server 端的 FIN 包,則說明 Server 端連接已釋放,接著就可以釋放自身的連接了,于是進入 TIME_WAIT
狀態,開始釋放資源,在經過設置的 2個 MSL 時間后,狀態最終遷移到 CLOSE
說明連接成功關閉,一次 TCP 4次揮手 關閉連接的過程結束。
通常會出現狀態滯留的情況有下面幾種:
- Client 處于 FIN_WAIT1 , Server 處于 ESTABLISHED => 這種情況通常是連接異常,socket.close() 發送的 FIN 包對端無法收到。由于 TCP FIN_WAIT 自身有 Timeout, 在 Timeout 后如果還沒有收到響應,則會停止等待。這種情況在 DDoS 攻擊中比較常見,Server 端在某一時刻需要處理大量 FIN_WAIT1 時就會卡死。解決方法是修改
/etc/sysctl.conf
的net.ipv4.tcp_fin_timeout
來提高 Timeout 值,保證大量連接能正常在超時時間內收到響應,當然這對服務器負載有要求。而如果是異常 ip 在某時間段內發送大量流量的 DDoS 攻擊,則可以在 iptable 上手動封 ip 或者開啟防火墻。 - Client 處于 FIN_WAIT2, Server 處于 CLOSE_WAIT => 這種情況通常是 Server 端還在使用連接進行讀寫或資源還未釋放完,所以還沒主動往對端發送 FIN 包進入 LAST_ACK 狀態,連接一直處于掛起的狀態。這種情況需要去檢查是否有資源未釋放或者代碼阻塞的問題。通常來說 CLOSE_WAIT 的持續時間應該較短,如果出現長時間的掛起,那么應該是代碼出了問題。
- Client 出于 TIME_WAIT, Server 處于 LAST_ACK => 首先 TIME_WAIT 需要等待 2個 MSL (Max Segment Lifetime) 時間,這個時間是確保 TCP 段能夠被接收到的最大壽命。默認是 60 s 。解決方案是: 1. 調整內核參數
/etc/sysctl.conf
中的net.ipv4.tcp_tw_recycle = 1
確保 TIME_WAIT 狀態的連接能夠快速回收,或者縮短 MSL 時間。 2. 檢查是否有些連接可以使用 keepalive 狀態來減少連接數。
此外,如果在單臺服務器上并且不做負載均衡而處理大量連接的話,可以在 /proc/sys/net/ipv4/ip_local_port_range
中減少端口的極限值,限制每個時間段的最大端口使用數,從而保證服務器的穩定性,一旦出現大量的 TIME_WAIT 阻塞后續連接,是比較致命的。
Socket.terminate() 和 Socket.close()
此外還遇到了另一個小問題,在關閉連接時,一開始用的是 socket.terminate()
,然而 netstat
時卻發現大量連接沒有釋放,后來發現 Python Socket 的 terminate()
只是發送 socket.SHUT_WR
和 socket.SHUT_RD
來關閉通道的讀寫權限而并沒有釋放連接句柄。導致了連接已經無法使用,但仍然處于 ESTABLISHED
狀態。
解決方法就是使用 socket.close()
來替換 socket.terminate()
后來又看到如果是 DDoS 攻擊的話,可能會阻塞住 socket.close()
,導致后續連接未關閉,大量流量進入服務器。
所以比較好的方式是在 socket.close()
之前先調用 socket.terminate()
關閉通道的讀寫權限,再調用 socket.close()