再敘TIME_WAIT

之因此起這樣一個題目是由於好久之前我曾經寫過一篇介紹TIME_WAIT的文章,不過當時基本屬於淺嘗輒止,並沒深刻說明問題的前因後果,碰巧這段時間反覆被別人問到相關的問題,讓我以爲有必要全面總結一下,以備不時之需。 html

討論前你們能夠拿手頭的服務器摸摸底,記住「ss」比「netstat」快: linux

shell> ss -ant | awk '
    NR>1 {++s[$1]} END {for(k in s) print k,s[k]}
'

若是你只是想單獨查詢一下TIME_WAIT的數量,那麼還能夠更簡單一些: android

shell> cat /proc/net/sockstat

我猜你必定被巨大無比的TIME_WAIT網絡鏈接總數嚇到了!以我我的的經驗,對於一臺繁忙的Web服務器來講,若是主要以短鏈接爲主,那麼其 TIME_WAIT網絡鏈接總數極可能會達到幾萬,甚至十幾萬。雖然一個TIME_WAIT網絡鏈接耗費的資源無非就是一個端口、一點內存,可是架不住基 數大,因此這始終是一個須要面對的問題。 git

爲何會存在TIME_WAIT?

TCP在創建鏈接的時候須要握手,同理,在關閉鏈接的時候也須要握手。爲了更直觀的說明關閉鏈接時握手的過程,咱們引用「The TCP/IP Guide」中的例子github

TCP Close

TCP Close shell

由於TCP鏈接是雙向的,因此在關閉鏈接的時候,兩個方向各自都須要關閉。先發FIN包的一方執行的是主動關閉;後發FIN包的一方執行的是被動關閉。主動關閉的一方會進入TIME_WAIT狀態,而且在此狀態停留兩倍的MSL時長。 安全

穿插一點MSL的知識:MSL指的是報文段的最大生存時間,若是報文段在網絡活動了MSL時間,尚未被接收,那麼會被丟棄。關於MSL的大小,RFC 793協議中給出的建議是兩分鐘,不過實際上不一樣的操做系統可能有不一樣的設置,以Linux爲例,一般是半分鐘,兩倍的MSL就是一分鐘,也就是60秒,而且這個數值是硬編碼在內核中的,也就是說除非你從新編譯內核,不然無法修改它: 服務器

#define TCP_TIMEWAIT_LEN (60*HZ)

若是每秒的鏈接數是一千的話,那麼一分鐘就可能會產生六萬個TIME_WAIT。 網絡

爲何主動關閉的一方不直接進入CLOSED狀態,而是進入TIME_WAIT狀態,而且停留兩倍的MSL時長呢?這是由於TCP是創建在不可靠網 絡上的可靠的協議。例子:主動關閉的一方收到被動關閉的一方發出的FIN包後,迴應ACK包,同時進入TIME_WAIT狀態,可是由於網絡緣由,主動關 閉的一方發送的這個ACK包極可能延遲,從而觸發被動鏈接一方重傳FIN包。極端狀況下,這一去一回,就是兩倍的MSL時長。若是主動關閉的一方跳過 TIME_WAIT直接進入CLOSED,或者在TIME_WAIT停留的時長不足兩倍的MSL,那麼當被動關閉的一方早先發出的延遲包到達後,就可能出 現相似下面的問題: tcp

  • 舊的TCP鏈接已經不存在了,系統此時只能返回RST包
  • 新的TCP鏈接被創建起來了,延遲包可能干擾新的鏈接

不論是哪一種狀況都會讓TCP再也不可靠,因此TIME_WAIT狀態有存在的必要性。

如何控制TIME_WAIT的數量?

從前面的描述咱們能夠得出這樣的結論:TIME_WAIT這東西沒有的話不行,不過太多可能也是個麻煩事。下面讓咱們看看有哪些方法能夠控制TIME_WAIT數量,這裏只說一些常規方法,另一些諸如SO_LINGER之類的方法太過偏門,略過不談。

ip_conntrack:顧名思義就是跟蹤鏈接。一旦激活了此模塊,就能在系統參數裏發現不少用來控制網絡鏈接狀態超時的設置,其中天然也包括TIME_WAIT:

shell> modprobe ip_conntrack
shell> sysctl net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait

咱們能夠嘗試縮小它的設置,好比十秒,甚至一秒,具體設置成多少合適取決於網絡狀況而定,固然也能夠參考相關的案例。不過就個人我的意見來講,ip_conntrack引入的問題比解決的還多,好比性能會大幅降低,因此不建議使用。

tcp_tw_recycle:顧名思義就是回收TIME_WAIT鏈接。能夠說這個內核參數已經變成了大衆處理TIME_WAIT的萬金油,若是你在網絡上搜索TIME_WAIT的解決方案,十有八九會推薦設置它,不過這裏隱藏着一個不易察覺的陷阱

當多個客戶端經過NAT方式聯網並與服務端交互時,服務端看到的是同一個IP,也就是說對服務端而言這些客戶端實際上等同於一個,惋惜因爲這些客戶端的時間戳可能存在差別,因而乎從服務端的視角看,即可能出現時間戳錯亂的現象,進而直接致使時間戳小的數據包被丟棄。參考:tcp_tw_recycle和tcp_timestamps致使connect失敗問題

tcp_tw_reuse:顧名思義就是複用TIME_WAIT鏈接。當建立新鏈接的時候,若是可能的話會考慮 複用相應的TIME_WAIT鏈接。一般認爲「tcp_tw_reuse」比「tcp_tw_recycle」安全一些,這是由於一來TIME_WAIT 建立時間必須超過一秒纔可能會被複用;二來只有鏈接的時間戳是遞增的時候纔會被複用。官方文檔裏是這樣說的:若是從協議視角看它是安全的,那麼就可使用。這簡直就是外交辭令啊!按個人見解,若是網絡比較穩定,好比都是內網鏈接,那麼就能夠嘗試使用。

不過須要注意的是在哪裏使用,既然咱們要複用鏈接,那麼固然應該在鏈接的發起方使用,而不能在被鏈接方使用。舉例來講:客戶端向服務端發起HTTP 請求,服務端響應後主動關閉鏈接,因而TIME_WAIT便留在了服務端,此類狀況使用「tcp_tw_reuse」是無效的,由於服務端是被鏈接方,所 以不存在複用鏈接一說。讓咱們延伸一點來看,好比說服務端是PHP,它查詢另外一個MySQL服務端,而後主動斷開鏈接,因而TIME_WAIT就落在了 PHP一側,此類狀況下使用「tcp_tw_reuse」是有效的,由於此時PHP相對於MySQL而言是客戶端,它是鏈接的發起方,因此能夠複用鏈接。

說明:若是使用tcp_tw_reuse,請激活tcp_timestamps,不然無效。

tcp_max_tw_buckets:顧名思義就是控制TIME_WAIT總數。官網文檔說這個選項只是爲了阻止一些簡單的DoS攻擊,日常不要人爲的下降它。若是縮小了它,那麼系統會將多餘的TIME_WAIT刪除掉,日誌裏會顯示:「TCP: time wait bucket table overflow」。

須要提醒你們的是物極必反,曾經看到有人把「tcp_max_tw_buckets」設置成0,也就是說徹底拋棄TIME_WAIT,這就有些冒險了,用一句圍棋諺語來講:入界宜緩。

有時候,若是咱們換個角度去看問題,每每能獲得四兩撥千斤的效果。前面提到的例子:客戶端向服務端發起HTTP請求,服務端響應後主動關閉鏈接,於 是TIME_WAIT便留在了服務端。這裏的關鍵在於主動關閉鏈接的是服務端!在關閉TCP鏈接的時候,先出手的一方註定逃不開TIME_WAIT的宿 命,套用一句歌詞:把個人悲傷留給本身,你的美麗讓你帶走。若是客戶端可控的話,那麼在服務端打開KeepAlive,儘量不讓服務端主動關閉鏈接,而讓客戶端主動關閉鏈接,如此一來問題便迎刃而解了。

參考文檔:

  1. tcp短鏈接TIME_WAIT問題解決方法大全(1)——高屋建瓴
  2. tcp短鏈接TIME_WAIT問題解決方法大全(2)——SO_LINGER
  3. tcp短鏈接TIME_WAIT問題解決方法大全(3)——tcp_tw_recycle
  4. tcp短鏈接TIME_WAIT問題解決方法大全(4)——tcp_tw_reuse
  5. tcp短鏈接TIME_WAIT問題解決方法大全(5)——tcp_max_tw_buckets
相關文章
相關標籤/搜索