轉載:http://huoding.com/2013/12/31/316
http://blog.csdn.net/lxnkobe/article/details/7525317
http://kerry.blog.51cto.com/172631/105233/
討論前大家可以拿手頭的服務器摸摸底,記住「ss」比「netstat」快:
shell> ss -ant | awk '
NR>1 {++s[$1]} END {for(k in s) print k,s[k]}
'
如果你只是想單獨查詢一下TIME_WAIT的數量,那么還可以更簡單一些:
shell> cat /proc/net/sockstat
我猜你一定被巨大無比的TIME_WAIT網絡連接總數嚇到了!以我個人的經驗,對於一台繁忙的Web服務器來說,如果主要以短連接為主,那么其TIME_WAIT網絡連接總數很可能會達到幾萬,甚至十幾萬。雖然一個TIME_WAIT網絡連接耗費的資源無非就是一個端口、一點內存,但是架不住基數大,所以這始終是一個需要面對的問題。(close_wait狀態就是對端所處的狀態。比如客戶端是time_wait,服務端就是close_wait狀態。)
TCP終止連接一般是需要交換四個分節。具體來看:
1、 應用進程(active close)首先調用close,於是導致TCP發送一個FIN分節,表示數據已分送完畢,請求關閉套接字。
2、 另一端應用進程(passive close)接受收到FIN,並由該端的TCP確認(確認的過程是TCP發送ACK分節給對端套接字)。FIN的接受也作為文件結束符傳遞給上層應用進程。這里的文件結束符並非應用進程的EOF,在TCP字節流中,EOF的讀或寫通過收發一個特殊的FIN分節來實現。
3、 另端(passive close)應用進程在接受到文件束符后,會調用close關閉它的套接字,這導致該端的TCP也發送了一個FIN分節。
4、 主動關閉端(active close)接受到這個FIN后,TCP對它進行確認。(TCP發送ACK分節,值得注意的是主動關閉端在未接受到FIN之前,它的狀態就是TIME_WAIT)。
這張圖在google image中,花了五六分鍾才找到,覺得這張圖是最直觀、易懂的。
TIME_OUT狀態的存在的意義
從圖中,很清晰的看到TIME_WAIT狀態發生在了active close 端,產生的時間點是發送ACK K+1 分節之后,原因是防止ACK分節在網絡中丟失(lost),此時passive close進入LAST_ACK狀態,意為等待ACK分節,如果此時ACK分節真的丟失了(passive close端的LAST_ACK超時),那么passive close端將會再次發送一個FIN K分節給對端。這就是為什么在圖中,出現兩次FIN的分節。
TIME_OUT存在的理由用術語來描述,摘自UNIX Network Programming Vol1 中:
1、 可靠地實現TCP全雙工連接的終止。
2、 允許老的重復分節在網絡中消逝。
TIME_OUT狀態的持續時間
圖中標明了TIME_OUT狀態的持續時間是最長分節生命周期(MSL)的兩倍,即2MSL。RFC中的建議值是2分鍾,Berkeley的實現傳統上使用的是30秒,那么這意味着TIME_WAIT狀態的延遲是在1~4分鍾之間。TIME_WAIT是TCP協議用以保證被重新分配的socket不會受到之前殘留的延遲重發報文影響的機制,是必要的邏輯保證。
(My:Linux的也是30s,編譯在系統內核。要改的話,除非重新編譯系統內核。)
既然TIME_OUT狀態的存在是有其意義的,為什么這么多人對其如此敏感,對於CS的模式,大多是由客戶機主動關閉連接,這也避免了TIME_OUT產生於服務端,但對於某些協議,如HTTP則是由服務器執行主動關閉的。(My:一般來說,盡量避免服務端過多的time_wait狀態。因為這會導致socket可用的端口越來越少,影響系統的運行。【特別是短連接特別多的】系統。可以試着讓系統更多的保持keepalive狀態,可能會好點。)
解決方法:
【1】.如果可以的話,減小time_wait的時間。
編輯文件,加入以下內容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然后執行
/sbin/sysctl -p 讓參數生效。
net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連接,默認為0,表示關閉;
net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。
net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間。
JDK對該選項的定義為:
- public void setSoLinger(boolean on, int linger) throws SocketException;
兩個參數將產生下列三種情形:
1、 on 為 false,則該選項關閉,linger 的值被忽略,這就是TCP的缺省設置,close立即返回,如果可能將會傳輸未發送的數據給對端;
2、 設置on為true,linger大於0(我在很多文章中看到這里寫的是非0,但Java中,給該選項設置小於0會拋出” invalid value for SO_LINGER”異常)那么當close某個連接時,內核將拖延一段時間。即linger的時間(linger的單位為秒,最大值為65535)。這里的拖延(close 阻塞)是相對於BIO來講,如果套接字先前被設置為非阻塞(NIO),那么將不等待close完成,即使linger > 0也是如此。如果套接字發送緩沖區中仍然殘留數據,那么close線程將被投入睡眠,直到所有數據都已發送完,並且均被對端確認或者拖延時間到,close才會被喚醒。
這里有一個原則:設置SO_LINGER套接字選項后,close的成功返回只是告訴我們早先發送的數據(包括FIN)已由對端TCP確認,而不能告訴我們對端的應用進程是否已讀取到數據,但如果不設置該套接字選項,那我們連對端TCP是否確認了數據都不知道;
3、 設置on為true,linger 為0,那么當close某個連接時,TCP夭折該連接。也就是說TCP將丟棄保留在發送緩沖區中的任何數據,僅僅給對端發送一個RST分節,而沒有通常所說的四分組終止序列,這樣一來避免了TIME_WAIT狀態。
然而在2MSL秒內創建該連接的另一個化身,會導致老的重復分節被不正確地遞送到新的化身上。這樣的情況,有一個替代,就是TCP的 SO_REUSEADDR選項。
從前面的描述我們可以得出這樣的結論: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,盡可能不讓服務端主動關閉連接,而讓客戶端主動關閉連接,如此一來問題便迎刃而解了。

