<div style="text-align: right">LexusLee</div>
背景
最近踩到一個(gè) "Socket 連接持續(xù)處于 Fin_Wait2 和 Close_Wait 狀態(tài)無(wú)法關(guān)閉" 的坑中吸占。起因是在維護(hù)大量連接時(shí)調(diào)用 socket.close()
時(shí)所刀,看到部分連接并沒(méi)有正常關(guān)閉撰洗,而是從 ESTABLISHED
的狀態(tài)變成 FIN_WAIT2
并且連接狀態(tài)沒(méi)有后續(xù)遷移,而對(duì)端的連接狀態(tài)則是從 ESTABLISHED
變成了 CLOSE_WAIT
措拇。
后來(lái)發(fā)現(xiàn)這和 TCP/IP 棧的4次揮手?jǐn)嚅_(kāi)連接有關(guān)我纪,列出一些踩坑時(shí)的收獲。
Socket 連接關(guān)閉的流程
先看一張 Socket 關(guān)閉連接的狀態(tài)遷移路徑圖:
在 Client 端調(diào)用 socket.close()
時(shí)丐吓,首先會(huì)往對(duì)端(即 Server 端)發(fā)送一個(gè) FIN 包浅悉,接著將自身的狀態(tài)置為 FIN_WAIT1
,此時(shí)主動(dòng)關(guān)閉端(即 Client 端)處于持續(xù)等待接收對(duì)端的響應(yīng) FIN 包的 ACK 回應(yīng)狀態(tài)券犁,此時(shí)對(duì)端的狀態(tài)是處于 ESTABLISHED
术健,一旦收到了 Client 發(fā)來(lái)的 close 連接請(qǐng)求,就回應(yīng)一個(gè) FIN 包粘衬,表示收到該請(qǐng)求了荞估,并將自身狀態(tài)置為 CLOSE_WAIT
,這時(shí)開(kāi)始等待 Server 端的應(yīng)用層向 Client 端發(fā)起 close 請(qǐng)求稚新。
這時(shí) Client 端一旦收到 Server 端對(duì)第一個(gè) FIN 包的回應(yīng) ACK 就會(huì)將進(jìn)入下一個(gè)狀態(tài) FIN_WAIT_2
來(lái)等待 Server 發(fā)起斷開(kāi)連接的 FIN 包勘伺。在FIN_WAIT_1 的 time_wait 中, Server 端會(huì)發(fā)起 close 請(qǐng)求褂删,向 Client 端發(fā)送 FIN 包飞醉,并將自身狀態(tài)從 CLOSE_WAIT
置為 LAST_ACK
,表示 Server 端的連接資源開(kāi)始釋放了屯阀。同時(shí) Client 端正處于 FIN_WAIT2
狀態(tài)缅帘,一旦接收到 Server 端的 FIN 包轴术,則說(shuō)明 Server 端連接已釋放,接著就可以釋放自身的連接了钦无,于是進(jìn)入 TIME_WAIT
狀態(tài)逗栽,開(kāi)始釋放資源,在經(jīng)過(guò)設(shè)置的 2個(gè) MSL 時(shí)間后失暂,狀態(tài)最終遷移到 CLOSE
說(shuō)明連接成功關(guān)閉彼宠,一次 TCP 4次揮手 關(guān)閉連接的過(guò)程結(jié)束。
通常會(huì)出現(xiàn)狀態(tài)滯留的情況有下面幾種:
- Client 處于 FIN_WAIT1 , Server 處于 ESTABLISHED => 這種情況通常是連接異常弟塞,socket.close() 發(fā)送的 FIN 包對(duì)端無(wú)法收到兵志。由于 TCP FIN_WAIT 自身有 Timeout, 在 Timeout 后如果還沒(méi)有收到響應(yīng),則會(huì)停止等待宣肚。這種情況在 DDoS 攻擊中比較常見(jiàn),Server 端在某一時(shí)刻需要處理大量 FIN_WAIT1 時(shí)就會(huì)卡死悠栓。解決方法是修改
/etc/sysctl.conf
的net.ipv4.tcp_fin_timeout
來(lái)提高 Timeout 值霉涨,保證大量連接能正常在超時(shí)時(shí)間內(nèi)收到響應(yīng),當(dāng)然這對(duì)服務(wù)器負(fù)載有要求惭适。而如果是異常 ip 在某時(shí)間段內(nèi)發(fā)送大量流量的 DDoS 攻擊笙瑟,則可以在 iptable 上手動(dòng)封 ip 或者開(kāi)啟防火墻。 - Client 處于 FIN_WAIT2, Server 處于 CLOSE_WAIT => 這種情況通常是 Server 端還在使用連接進(jìn)行讀寫(xiě)或資源還未釋放完癞志,所以還沒(méi)主動(dòng)往對(duì)端發(fā)送 FIN 包進(jìn)入 LAST_ACK 狀態(tài)往枷,連接一直處于掛起的狀態(tài)。這種情況需要去檢查是否有資源未釋放或者代碼阻塞的問(wèn)題凄杯。通常來(lái)說(shuō) CLOSE_WAIT 的持續(xù)時(shí)間應(yīng)該較短错洁,如果出現(xiàn)長(zhǎng)時(shí)間的掛起,那么應(yīng)該是代碼出了問(wèn)題戒突。
- Client 出于 TIME_WAIT, Server 處于 LAST_ACK => 首先 TIME_WAIT 需要等待 2個(gè) MSL (Max Segment Lifetime) 時(shí)間屯碴,這個(gè)時(shí)間是確保 TCP 段能夠被接收到的最大壽命。默認(rèn)是 60 s 膊存。解決方案是: 1. 調(diào)整內(nèi)核參數(shù)
/etc/sysctl.conf
中的net.ipv4.tcp_tw_recycle = 1
確保 TIME_WAIT 狀態(tài)的連接能夠快速回收导而,或者縮短 MSL 時(shí)間。 2. 檢查是否有些連接可以使用 keepalive 狀態(tài)來(lái)減少連接數(shù)隔崎。
此外今艺,如果在單臺(tái)服務(wù)器上并且不做負(fù)載均衡而處理大量連接的話,可以在 /proc/sys/net/ipv4/ip_local_port_range
中減少端口的極限值爵卒,限制每個(gè)時(shí)間段的最大端口使用數(shù)虚缎,從而保證服務(wù)器的穩(wěn)定性,一旦出現(xiàn)大量的 TIME_WAIT 阻塞后續(xù)連接技潘,是比較致命的遥巴。
Socket.terminate() 和 Socket.close()
此外還遇到了另一個(gè)小問(wèn)題千康,在關(guān)閉連接時(shí),一開(kāi)始用的是 socket.terminate()
铲掐,然而 netstat
時(shí)卻發(fā)現(xiàn)大量連接沒(méi)有釋放拾弃,后來(lái)發(fā)現(xiàn) Python Socket 的 terminate()
只是發(fā)送 socket.SHUT_WR
和 socket.SHUT_RD
來(lái)關(guān)閉通道的讀寫(xiě)權(quán)限而并沒(méi)有釋放連接句柄。導(dǎo)致了連接已經(jīng)無(wú)法使用摆霉,但仍然處于 ESTABLISHED
狀態(tài)豪椿。
解決方法就是使用 socket.close()
來(lái)替換 socket.terminate()
后來(lái)又看到如果是 DDoS 攻擊的話,可能會(huì)阻塞住 socket.close()
携栋,導(dǎo)致后續(xù)連接未關(guān)閉搭盾,大量流量進(jìn)入服務(wù)器。
所以比較好的方式是在 socket.close()
之前先調(diào)用 socket.terminate()
關(guān)閉通道的讀寫(xiě)權(quán)限婉支,再調(diào)用 socket.close()