再敘TIME_WAIT(轉(zhuǎn))
再敘TIME_WAIT
之所以起這樣一個題目是因為很久以前我曾經(jīng)寫過一篇介紹TIME_WAIT的文章,不過當(dāng)時基本屬于淺嘗輒止,并沒深入說明問題的來龍去脈,碰巧這段時間反復(fù)被別人問到相關(guān)的問題,讓我覺得有必要全面總結(jié)一下,以備不時之需。
討論前大家可以拿手頭的服務(wù)器摸摸底,記住「ss」比「netstat」快:
shell> ss -ant | awk ' NR>1 {++s[$1]} END {for(k in s) print k,s[k]} '
如果你只是想單獨查詢一下TIME_WAIT的數(shù)量,那么還可以更簡單一些:
shell> cat /proc/net/sockstat
我猜你一定被巨大無比的TIME_WAIT網(wǎng)絡(luò)連接總數(shù)嚇到了!以我個人的經(jīng)驗,對于一臺繁忙的Web服務(wù)器來說,如果主要以短連接為主,那么其TIME_WAIT網(wǎng)絡(luò)連接總數(shù)很可能會達(dá)到幾萬,甚至十幾萬。雖然一個TIME_WAIT網(wǎng)絡(luò)連接耗費的資源無非就是一個端口、一點內(nèi)存,但是架不住基數(shù)大,所以這始終是一個需要面對的問題。
為什么會存在TIME_WAIT?
TCP在建立連接的時候需要握手,同理,在關(guān)閉連接的時候也需要握手。為了更直觀的說明關(guān)閉連接時握手的過程,我們引用「The TCP/IP Guide」中的例子:
因為TCP連接是雙向的,所以在關(guān)閉連接的時候,兩個方向各自都需要關(guān)閉。先發(fā)FIN包的一方執(zhí)行的是主動關(guān)閉;后發(fā)FIN包的一方執(zhí)行的是被動關(guān)閉。主動關(guān)閉的一方會進(jìn)入TIME_WAIT狀態(tài),并且在此狀態(tài)停留兩倍的MSL時長。
穿插一點MSL的知識:MSL指的是報文段的最大生存時間,如果報文段在網(wǎng)絡(luò)活動了MSL時間,還沒有被接收,那么會被丟棄。關(guān)于MSL的大小,RFC 793協(xié)議中給出的建議是兩分鐘,不過實際上不同的操作系統(tǒng)可能有不同的設(shè)置,以Linux為例,通常是半分鐘,兩倍的MSL就是一分鐘,也就是60秒,并且這個數(shù)值是硬編碼在內(nèi)核中的,也就是說除非你重新編譯內(nèi)核,否則沒法修改它:
#define TCP_TIMEWAIT_LEN (60*HZ)
如果每秒的連接數(shù)是一千的話,那么一分鐘就可能會產(chǎn)生六萬個TIME_WAIT。
為什么主動關(guān)閉的一方不直接進(jìn)入CLOSED狀態(tài),而是進(jìn)入TIME_WAIT狀態(tài),并且停留兩倍的MSL時長呢?這是因為TCP是建立在不可靠網(wǎng)絡(luò)上的可靠的協(xié)議。例子:主動關(guān)閉的一方收到被動關(guān)閉的一方發(fā)出的FIN包后,回應(yīng)ACK包,同時進(jìn)入TIME_WAIT狀態(tài),但是因為網(wǎng)絡(luò)原因,主動關(guān)閉的一方發(fā)送的這個ACK包很可能延遲,從而觸發(fā)被動連接一方重傳FIN包。極端情況下,這一去一回,就是兩倍的MSL時長。如果主動關(guān)閉的一方跳過TIME_WAIT直接進(jìn)入CLOSED,或者在TIME_WAIT停留的時長不足兩倍的MSL,那么當(dāng)被動關(guān)閉的一方早先發(fā)出的延遲包到達(dá)后,就可能出現(xiàn)類似下面的問題:
- 舊的TCP連接已經(jīng)不存在了,系統(tǒng)此時只能返回RST包
- 新的TCP連接被建立起來了,延遲包可能干擾新的連接
不管是哪種情況都會讓TCP不再可靠,所以TIME_WAIT狀態(tài)有存在的必要性。
如何控制TIME_WAIT的數(shù)量?
從前面的描述我們可以得出這樣的結(jié)論:TIME_WAIT這東西沒有的話不行,不過太多可能也是個麻煩事。下面讓我們看看有哪些方法可以控制TIME_WAIT數(shù)量,這里只說一些常規(guī)方法,另外一些諸如SO_LINGER之類的方法太過偏門,略過不談。
ip_conntrack:顧名思義就是跟蹤連接。一旦激活了此模塊,就能在系統(tǒng)參數(shù)里發(fā)現(xiàn)很多用來控制網(wǎng)絡(luò)連接狀態(tài)超時的設(shè)置,其中自然也包括TIME_WAIT:
shell> modprobe ip_conntrack shell> sysctl net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait
我們可以嘗試縮小它的設(shè)置,比如十秒,甚至一秒,具體設(shè)置成多少合適取決于網(wǎng)絡(luò)情況而定,當(dāng)然也可以參考相關(guān)的案例。不過就我的個人意見來說,ip_conntrack引入的問題比解決的還多,比如性能會大幅下降,所以不建議使用。
tcp_tw_recycle:顧名思義就是回收TIME_WAIT連接。可以說這個內(nèi)核參數(shù)已經(jīng)變成了大眾處理TIME_WAIT的萬金油,如果你在網(wǎng)絡(luò)上搜索TIME_WAIT的解決方案,十有八九會推薦設(shè)置它,不過這里隱藏著一個不易察覺的陷阱:
當(dāng)多個客戶端通過NAT方式聯(lián)網(wǎng)并與服務(wù)端交互時,服務(wù)端看到的是同一個IP,也就是說對服務(wù)端而言這些客戶端實際上等同于一個,可惜由于這些客戶端的時間戳可能存在差異,于是乎從服務(wù)端的視角看,便可能出現(xiàn)時間戳錯亂的現(xiàn)象,進(jìn)而直接導(dǎo)致時間戳小的數(shù)據(jù)包被丟棄。參考:tcp_tw_recycle和tcp_timestamps導(dǎo)致connect失敗問題。
tcp_tw_reuse:顧名思義就是復(fù)用TIME_WAIT連接。當(dāng)創(chuàng)建新連接的時候,如果可能的話會考慮復(fù)用相應(yīng)的TIME_WAIT連接。通常認(rèn)為「tcp_tw_reuse」比「tcp_tw_recycle」安全一些,這是因為一來TIME_WAIT創(chuàng)建時間必須超過一秒才可能會被復(fù)用;二來只有連接的時間戳是遞增的時候才會被復(fù)用。官方文檔里是這樣說的:如果從協(xié)議視角看它是安全的,那么就可以使用。這簡直就是外交辭令啊!按我的看法,如果網(wǎng)絡(luò)比較穩(wěn)定,比如都是內(nèi)網(wǎng)連接,那么就可以嘗試使用。
不過需要注意的是在哪里使用,既然我們要復(fù)用連接,那么當(dāng)然應(yīng)該在連接的發(fā)起方使用,而不能在被連接方使用。舉例來說:客戶端向服務(wù)端發(fā)起HTTP請求,服務(wù)端響應(yīng)后主動關(guān)閉連接,于是TIME_WAIT便留在了服務(wù)端,此類情況使用「tcp_tw_reuse」是無效的,因為服務(wù)端是被連接方,所以不存在復(fù)用連接一說。讓我們延伸一點來看,比如說服務(wù)端是PHP,它查詢另一個MySQL服務(wù)端,然后主動斷開連接,于是TIME_WAIT就落在了PHP一側(cè),此類情況下使用「tcp_tw_reuse」是有效的,因為此時PHP相對于MySQL而言是客戶端,它是連接的發(fā)起方,所以可以復(fù)用連接。
說明:如果使用tcp_tw_reuse,請激活tcp_timestamps,否則無效。
tcp_max_tw_buckets:顧名思義就是控制TIME_WAIT總數(shù)。官網(wǎng)文檔說這個選項只是為了阻止一些簡單的DoS攻擊,平常不要人為的降低它。如果縮小了它,那么系統(tǒng)會將多余的TIME_WAIT刪除掉,日志里會顯示:「TCP: time wait bucket table overflow」。
需要提醒大家的是物極必反,曾經(jīng)看到有人把「tcp_max_tw_buckets」設(shè)置成0,也就是說完全拋棄TIME_WAIT,這就有些冒險了,用一句圍棋諺語來說:入界宜緩。
…
有時候,如果我們換個角度去看問題,往往能得到四兩撥千斤的效果。前面提到的例子:客戶端向服務(wù)端發(fā)起HTTP請求,服務(wù)端響應(yīng)后主動關(guān)閉連接,于是TIME_WAIT便留在了服務(wù)端。這里的關(guān)鍵在于主動關(guān)閉連接的是服務(wù)端!在關(guān)閉TCP連接的時候,先出手的一方注定逃不開TIME_WAIT的宿命,套用一句歌詞:把我的悲傷留給自己,你的美麗讓你帶走。如果客戶端可控的話,那么在服務(wù)端打開KeepAlive,盡可能不讓服務(wù)端主動關(guān)閉連接,而讓客戶端主動關(guān)閉連接,如此一來問題便迎刃而解了。
參考文檔: