你所不知道的TIME_WAIT和CLOSE_WAIT


你遇到過TIME_WAIT的問題嗎?

 

我相信很多都遇到過這個問題。一旦有用戶在喊:網絡變慢了, 這時在cli:

ss -s
Total: 4372 (kernel 5046)
TCP:   12111 (estab 3973, closed 8124, orphaned 0, synrecv 0, timewait 8123/0), ports 3878

Transport Total     IP        IPv6
*         5046      -         -
RAW       0         0         0
UDP       480       479       1
TCP       3987      3982      5
INET      4467      4461      6
FRAG      0         0         0

 

然后打開Google,輸入關鍵詞:too many timewait。一定能找到解決方案,而排在最前面或者被很多人到處轉載的解決方案一定是:

 

打開 sysctl.conf 文件,修改以下幾個參數, 然后sysctl -p:

 

  • net.ipv4.tcp_tw_recycle = 1

  • net.ipv4.tcp_tw_reuse = 1

  • net.ipv4.tcp_timestamps = 1

 

你也會被告知,開啟tw_recylce和tw_reuse一定需要timestamps的支持,而且這些配置一般不建議開啟,但是對解決TIME_WAIT很多的問題,有很好的用處。

接下來,你就直接修改了這幾個參數,sysctl -p 一下,發現,咦,沒幾分鍾,TIME_WAIT的數量真的降低了,也沒發現哪個用戶說有問題,然后就沒有然后了。

做到這一步,相信50%或者更高比例的開發就已經止步了。問題好像解決了,但是,要徹底理解並解決這個問題,可能就沒這么簡單,或者說,還有很長的路要走!

 

什么是TIME-WAIT和CLOSE-WAIT?

所謂,要解決問題,就要先理解問題。隨便改兩行代碼,發現bug“沒有了”,也不是bug真的沒有了,只是隱藏在更深的地方,你沒有發現,或者以你的知識水平,你無法發現而已。

大家知道,由於socket是全雙工的工作模式,一個socket的關閉,是需要四次握手來完成的

  • 主動關閉連接的一方,調用close();協議層發送FIN包

  • 被動關閉的一方收到FIN包后,協議層回復ACK;然后被動關閉的一方,進入CLOSE_WAIT狀態,主動關閉的一方等待對方關閉,則進入FIN_WAIT_2狀態;此時,主動關閉的一方 等待 被動關閉一方的應用程序,調用close操作

  • 被動關閉的一方在完成所有數據發送后,調用close()操作;此時,協議層發送FIN包給主動關閉的一方,等待對方的ACK,被動關閉的一方進入LAST_ACK狀態

  • 主動關閉的一方收到FIN包,協議層回復ACK;此時,主動關閉連接的一方,進入TIME_WAIT狀態;而被動關閉的一方,進入CLOSED狀態

  • 等待2MSL時間,主動關閉的一方,結束TIME_WAIT,進入CLOSED狀態

 

通過上面的一次socket關閉操作,你可以得出以下幾點:

  1. 主動關閉連接的一方 - 也就是主動調用socket的close操作的一方,最終會進入TIME_WAIT狀態

  2. 被動關閉連接的一方,有一個中間狀態,即CLOSE_WAIT,因為協議層在等待上層的應用程序,主動調用close操作后才主動關閉這條連接

  3. TIME_WAIT會默認等待2MSL時間后,才最終進入CLOSED狀態;

  4. 在一個連接沒有進入CLOSED狀態之前,這個連接是不能被重用的!

 

 

 

 

所以,這里憑你的直覺,TIME_WAIT並不可怕(not really,后面講),CLOSE_WAIT才可怕,因為CLOSE_WAIT很多,表示說要么是你的應用程序寫的有問題,沒有合適的關閉socket;要么是說,你的服務器CPU處理不過來(CPU太忙)或者你的應用程序一直睡眠到其它地方(鎖,或者文件I/O等等),你的應用程序獲得不到合適的調度時間,造成你的程序沒法真正的執行close操作。

 

 

 圖上能看出,Socket是循環利用,回收鏈接后應該是CLOSED狀態。

 

Socket連接到底是個什么概念? 

大家經常提socket,那么,到底什么是一個socket?其實,socket就是一個 五元組,包括:

  1. 源IP

  2. 源端口

  3. 目的IP

  4. 目的端口

  5. 類型:TCP or UDP

 

這個五元組,即標識了一條可用的連接。

例如,如果你的本地出口IP是180.172.35.150,那么你的瀏覽器在連接某一個Web服務器,例如百度的時候,這條socket連接的四元組可能就是:

 

[180.172.35.150:45678, tcp, 180.97.33.108:80]

 

源IP為你的出口IP地址 180.172.35.150,源端口為隨機端口 45678,目的IP為百度的某一個負載均衡服務器IP 180.97.33.108,端口為HTTP標准的80端口。

如果這個時候,你再開一個瀏覽器,訪問百度,將會產生一條新的連接:

[180.172.35.150:43622, tcp, 180.97.33.108:80]

這條新的連接的源端口為一個新的隨機端口 43622

 

第二個問題,TIME_WAIT有什么用?

如果我們來做個類比的話,TIME_WAIT的出現,對應的是你的程序里的異常處理,它的出現,就是為了解決網絡的丟包和網絡不穩定所帶來的其他問題:

第一,防止前一個連接上延遲的數據包或者丟失重傳的數據包,被后面復用的連接, 我們繼續以 180.172.35.150:45678, tcp, 180.97.33.108:80 為例,前一個連接關閉后,此時你再次訪問百度,新的連接可能還是由180.172.35.150:45678, tcp, 180.97.33.108:80 這個五元組來表示,也就是源端口湊巧還是45678,錯誤的接收(異常:數據丟了,或者傳輸太慢了):

 

 

 

  • SEQ=3的第3個數據包丟失,重傳第一次,沒有得到ACK確認

  • 如果沒有TIME_WAIT,或者TIME_WAIT時間非常小,那么關閉的連接【180.172.35.150:45678, tcp, 180.97.33.108:80 的狀態變為了CLOSED,源端口可被再次利用】,馬上被重用【對180.97.33.108:80新建的連接,復用了之前的隨機端口45678】,並連續發送SEQ=1,2 的數據包

  • 此時,前面的連接上的SEQ=3的數據包再次重傳,同時,seq的序號剛好也是3(這個很重要,不然,SEQ的序號對不上,就會RST掉),此時,前面一個連接上的數據被后面的一個連接錯誤的接收

 

第二,確保連接方能在時間范圍內,關閉自己的連接。其實,也是因為丟包造成的,參見下圖:

  • 主動關閉方關閉了連接,發送了FIN;

  • 被動關閉方回復ACK同時也執行關閉動作,發送FIN包;此時,被動關閉的一方進入LAST_ACK狀態

  • 主動關閉的一方回去了ACK,主動關閉一方進入TIME_WAIT狀態;

  • 但是最后的ACK丟失,被動關閉的一方還繼續停留在LAST_ACK狀態

  • 此時,如果沒有TIME_WAIT的存在,或者說,停留在TIME_WAIT上的時間很短,則主動關閉的一方很快就進入了CLOSED狀態,也即是說,如果此時新建一個連接,源隨機端口如果被復用,在connect發送SYN包后,由於被動方仍認為這條連接【五元組】還在等待ACK,但是卻收到了SYN,則被動方會回復RST

  • 造成主動創建連接的一方,由於收到了RST,則連接無法成功 

所以,你看到了,TIME_WAIT的存在是很重要的,如果強制忽略TIME_WAIT,還是有很高的機率,造成數據粗亂,或者短暫性的連接失敗。

 

那么,為什么說,TIME_WAIT狀態會是持續2MSL(2倍的max segment lifetime)呢?這個時間可以通過修改內核參數調整嗎?第一,這個2MSL,是RFC 793里定義的,參見RFC的截圖標紅的部分:


這個定義,更多的是一種保障(IP數據包里的TTL,即數據最多存活的跳數,真正反應的才是數據在網絡上的存活時間),確保最后丟失了ACK,被動關閉的一方再次重發FIN並等待回復的ACK,一來一去兩個來回內核里,寫死了這個MSL的時間為:30秒(有讀者提醒,RFC里建議的MSL其實是2分鍾,但是很多實現都是30秒),所以TIME_WAIT的即為1分鍾

 

所以,再次回想一下前面的問題,如果一條連接,即使在四次握手關閉了,由於TIME_WAIT的存在,這個連接,在1分鍾之內,也無法再次被復用,那么,如果你用一台機器做壓測的客戶端,你一分鍾能發送多少並發連接請求?如果這台是一個負載均衡服務器,一台負載均衡服務器,一分鍾可以有多少個連接同時訪問后端的服務器呢?

 

TIME_WAIT很多,可怕嗎?

如果你通過 ss -tan state time-wait | wc -l 發現,系統中有很多TIME_WAIT,很多人都會緊張。多少算多呢?幾百幾千?如果是這個量級,其實真的沒必要緊張。第一,這個量級,因為TIME_WAIT所占用的內存很少很少;因為記錄和尋找可用的local port所消耗的CPU也基本可以忽略。

 

會占用內存嗎?當然任何你可以看到的數據,內核里都需要有相關的數據結構來保存這個數據啊。一條Socket處於TIME_WAIT狀態,它也是一條“存在”的socket,內核里也需要有保持它的數據:

  1. 內核里有保存所有連接的一個hash table,這個hash table里面既包含TIME_WAIT狀態的連接,也包含其他狀態的連接。主要用於有新的數據到來的時候,從這個hash table里快速找到這條連接。不同的內核對這個hash table的大小設置不同,你可以通過dmesg命令去找到你的內核設置的大小

  2. 還有一個hash table用來保存所有的bound ports,主要用於可以快速的找到一個可用的端口或者隨機端口: 

由於內核需要保存這些數據,必然,會占用一定的內存。

會消耗CPU嗎?當然!每次找到一個隨機端口,還是需要遍歷一遍bound ports的吧,這必然需要一些CPU時間。

TIME_WAIT很多,既占內存又消耗CPU,這也是為什么很多人,看到TIME_WAIT很多,就蠢蠢欲動的想去干掉他們。其實,如果你再進一步去研究,1萬條TIME_WAIT的連接,也就多消耗1M左右的內存,對現代的很多服務器,已經不算什么了。至於CPU,能減少它當然更好,但是不至於因為1萬多個hash item就擔憂。

如果,你真的想去調優,還是需要搞清楚別人的調優建議,以及調優參數背后的意義!

 

TIME_WAIT調優,你必須理解的幾個調優參數 

在具體的圖例之前,我們還是先解析一下相關的幾個參數存在的意義。

net.ipv4.tcp_timestamps

RFC 1323 在 TCP Reliability一節里,引入了timestamp的TCP option,兩個4字節的時間戳字段,其中第一個4字節字段用來保存發送該數據包的時間,第二個4字節字段用來保存最近一次接收對方發送到數據的時間。有了這兩個時間字段,也就有了后續優化的余地

tcp_tw_reuse 和 tcp_tw_recycle, tcp_fin_timeout就依賴這些時間字段

 

net.ipv4.tcp_tw_reuse 

字面意思,reuse TIME_WAIT狀態的連接。

時刻記住一條socket連接,就是那個五元組,出現TIME_WAIT狀態的連接,一定出現在主動關閉連接的一方。所以,當主動關閉連接的一方,再次向對方發起連接請求的時候(例如,客戶端關閉連接,客戶端再次連接服務端,此時可以復用了;負載均衡服務器,主動關閉后端的連接,當有新的HTTP請求,負載均衡服務器再次連接后端服務器,此時也可以復用),可以復用TIME_WAIT狀態的連接。

 通過字面解釋,以及例子說明,你看到了,tcp_tw_reuse應用的場景:某一方,需要不斷的通過“短連接”連接其他服務器,總是自己先關閉連接(TIME_WAIT在自己這方),關閉后又不斷的重新連接對方。

那么,當連接被復用了之后,延遲或者重發的數據包到達,新的連接怎么判斷,到達的數據是屬於復用后的連接,還是復用前的連接呢?那就需要依賴前面提到的兩個時間字段了。復用連接后,這條連接的時間被更新為當前的時間,當延遲的數據達到,延遲數據的時間是小於新連接的時間,所以,內核可以通過時間判斷出,延遲的數據可以安全的丟棄掉了。

這個配置,依賴於連接雙方,同時對timestamps的支持。同時,這個配置,僅僅影響outbound連接,即做為客戶端的角色,連接服務端[connect(dest_ip, dest_port)]時復用TIME_WAIT的socket。

 

net.ipv4.tcp_tw_recycle

 

字面意思,銷毀掉 TIME_WAIT。

當開啟了這個配置后,內核會快速的回收處於TIME_WAIT狀態的socket連接。多快?不再是2MSL,而是一個RTO(retransmission timeout,數據包重傳的timeout時間)的時間,這個時間根據RTT動態計算出來,但是遠小於2MSL。

有了這個配置,還是需要保障 丟失重傳或者延遲的數據包,不會被新的連接(注意,這里不再是復用了,而是之前處於TIME_WAIT狀態的連接已經被destroy掉了,新的連接,剛好是和某一個被destroy掉的連接使用了相同的五元組而已)所錯誤的接收。在啟用該配置,當一個socket連接進入TIME_WAIT狀態后,內核里會記錄包括該socket連接對應的五元組中的對方IP等在內的一些統計數據,當然也包括從該對方IP所接收到的最近的一次數據包時間。當有新的數據包到達,只要時間晚於內核記錄的這個時間,數據包都會被統統的丟掉。

這個配置,依賴於連接雙方對timestamps的支持。同時,這個配置,主要影響到了inbound的連接(對outbound的連接也有影響,但是不是復用),即做為服務端角色,客戶端連進來,服務端主動關閉了連接,TIME_WAIT狀態的socket處於服務端,服務端快速的回收該狀態的連接。

由此,如果客戶端處於NAT的網絡(多個客戶端,同一個IP出口的網絡環境),如果配置了tw_recycle,就可能在一個RTO的時間內,只能有一個客戶端和自己連接成功(不同的客戶端發包的時間不一致,造成服務端直接把數據包丟棄掉)。

 

舉個重傳的例子:

1. Server 發送80個字節 Part1,seq = 1 

2. Server 發送120個字節Part2,Seq = 81

3. Server發送160個字節Part3,Seq = 201,此包由於其他原因丟失

4. Client收到前2個報文段,並發送ACK = 201

5. Server發送140個字節Part4, Seq = 361

7. Server收到Client對於前兩個報文段的ACK,將2個報文從窗口中移除,窗口有200個字節的余量

8. 報文3的重傳定時器到期,沒有收到ACK,進行重傳

9. 這個時候Client已經收到報文4,存放在緩沖區中,也不會發送ACK【累計通知,發送ACK就表示3也收到了】,等待報文3,報文3收到之后,一塊對3,4進行確認

10. Server收到確認之后,將報文3,4移除窗口,所有數據發送完成

 

這種方式會面臨一個問題:客戶端在等待報文3的時候,服務器如何處理報文4, 客戶端這個期間內並沒有發送任何報文,服務器並不知道報文3和報文4的狀態,報文4可能會丟失,也可能會被客戶端接收,那么如果超時了,我到底值該發送報文3 ,還是報文3和報文4 呢?

總結起來就是2中處理

1. 定時器溢出,重傳3

2. 定時器溢出,重傳3,4

對於怎么傳的問題,在RFC2018中已經提供了一種方案: SACK,    詳細可參考文章:TCP-IP詳解:SACK選項(Selective Acknowledgment)
 

負載均衡服務器 首先關閉連接 

 

在這種情況下,因為負載均衡服務器對Web服務器的連接,TIME_WAIT大都出現在負載均衡服務器上,所以,在負載均衡服務器上的配置:

  • net.ipv4.tcp_tw_reuse = 1 //盡量復用連接

  • net.ipv4.tcp_tw_recycle = 0 //不能保證客戶端不在NAT的網絡啊

 

在Web服務器上的配置為:

  • net.ipv4.tcp_tw_reuse = 1 //這個配置主要影響的是Web服務器到DB服務器的連接復用

  • net.ipv4.tcp_tw_recycle: 設置成1和0都沒有任何意義。想一想,在負載均衡和它的連接中,它是服務端,但是TIME_WAIT出現在負載均衡服務器上;它和DB的連接,它是客戶端,recycle對它並沒有什么影響,關鍵是reuse

 

Web服務器首先關閉來自負載均衡服務器的連接 

在這種情況下,Web服務器變成TIME_WAIT的重災區。負載均衡對Web服務器的連接,由Web服務器首先關閉連接,TIME_WAIT出現在Web服務器上;Web服務器對DB服務器的連接,由Web服務器關閉連接,TIME_WAIT也出現在它身上,此時,負載均衡服務器上的配置:

  • net.ipv4.tcp_tw_reuse:0 或者 1 都行,都沒有實際意義

  • net.ipv4.tcp_tw_recycle=0 //一定是關閉recycle

在Web服務器上的配置:

  • net.ipv4.tcp_tw_reuse = 1 //這個配置主要影響的是Web服務器到DB服務器的連接復用

  • net.ipv4.tcp_tw_recycle=1 //由於在負載均衡和Web服務器之間並沒有NAT的網絡,可以考慮開啟recycle,加速由於負載均衡和Web服務器之間的連接造成的大量TIME_WAIT

 

 

回答幾個大家提到的幾個問題

 

1. 請問我們所說連接池可以復用連接,是不是意味着,需要等到上個連接time wait結束后才能再次使用?

所謂連接池復用,復用的一定是活躍的連接,所謂活躍,第一表明連接池里的連接都是ESTABLISHED的,第二,連接池做為上層應用,會有定時的心跳去保持連接的活躍性。既然連接都是活躍的,那就不存在有TIME_WAIT的概念了,在上篇里也有提到,TIME_WAIT是在主動關閉連接的一方,在關閉連接后才進入的狀態。既然已經關閉了,那么這條連接肯定已經不在連接池里面了,即被連接池釋放了。

 

2. 想請問下,作為負載均衡的機器隨機端口使用完的情況下大量time_wait,不調整你文字里說的那三個參數,有其他的更好的方案嗎?

第一,隨機端口使用完,你可以通過調整/etc/sysctl.conf下的net.ipv4.ip_local_port_range配置,至少修改成 net.ipv4.ip_local_port_range=1024 65535,保證你的負載均衡服務器至少可以使用6萬個隨機端口,也即可以有6萬的反向代理到后端的連接,可以支持每秒1000的並發(想一想,因為TIME_WAIT狀態會持續1分鍾后消失,所以一分鍾最多有6萬,每秒1000);如果這么多端口都使用完了,也證明你應該加服務器了,或者,你的負載均衡服務器需要配置多個IP地址,或者,你的后端服務器需要監聽更多的端口和配置更多的IP(想一下socket的五元組)

第二,大量的TIME_WAIT,多大量?如果是幾千個,其實不用擔心,因為這個內存和CPU的消耗有一些,但是是可以忽略的。

第三,如果真的量很大,上萬上萬的那種,可以考慮,讓后端的服務器主動關閉連接,如果后端服務器沒有外網的連接只有負載均衡服務器的連接(主要是沒有NAT網絡的連接),可以在后端服務器上配置tw_recycle,然后同時,在負載均衡服務器上,配置tw_reuse。

 

3. 如果想深入的學習一下網絡方面的知識,有什么推薦的?

學習網絡比學一門編程語言“難”很多。所謂難,其實,是因為需要花很多的時間投入。我自己不算精通,只能說入門和理解。基本書可以推薦:《TCP/IP 協議詳解》,必讀;《TCP/IP高效編程:改善網絡程序的44個技巧》,必讀;《Unix環境高級編程》,必讀;《Unix網絡編程:卷一》,我只讀過卷一;另外,還需要熟悉一下網絡工具,tcpdump以及wireshark,我的notes里有一個一站式學習Wireshark:https://github.com/dafang/notebook/issues/114,也值得一讀。有了這些積累,可能就是一些實踐以及碎片化的學習和積累了。

 

排查示例:

TIME-WAIT連接數過多
[robin@a.test.com ~]$ ss -a | grep TIME-WAIT -c
12206
[robin@a.test.com ~]$ ss -s
Total: 483 (kernel 550)
TCP:   11362 (estab 200, closed 11058, orphaned 0, synrecv 0, timewait 11057/0), ports 6284

Transport Total     IP        IPv6
*         550       -         -
RAW       0         0         0
UDP       8         8         0
TCP       304       300       4
INET      312       308       4
FRAG      0         0         0

[robin@a.test.com ~]$ sudo netstat -a | grep TIME_WAIT | wc -l
11923
[robin@a.test.com ~]$

查看和哪個的連接數過多
[robin@a.test.com ~]$ ss -a | grep TIME-WAIT  | awk '{print $5}' | awk -F":" '{print $1}' | sort | uniq -c
| sort -k1 -rn | head
   4352 10.4.14.24
   1692 10.4.14.23
    316 10.32.120.13
    308 10.32.192.2
    300 10.32.188.7
    294 10.32.124.48
    285 10.32.121.8
    165 10.69.69.126
    153 10.23.43.30
    144 10.32.206.4
[robin@a.test.com ~]$
原因是本機器的服務通過VIP調用org服務,但是代碼中沒有主動釋放連接,導致的TIME-WAIT狀態過多

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM