系統調優,你所不知道的TIME_WAIT和CLOSE_WAIT

高性能網絡 | 你所不知道的TIME_WAIT和CLOSE_WAIT

2016-02-18 大房 大房說git

本文是我將最近兩篇文章,從新整理成一篇,方便收藏。若是你已經閱讀過前兩篇,而且已經作了收藏,能夠從新收藏本文便可。github


你有收藏和整理文章的習慣嗎?好好利用Evernote或者印象筆記,不要吝嗇那點年費,你值得購買,並養成收藏和整理的習慣!web


本文源於你們在公衆號裏面的留言,既然不少人都搞不清楚TIME_WAIT和CLOSE_WAIT,那麼小胖哥今天仍是抽個時間,統一幫你們理理概念吧。編程


你遇到過TIME_WAIT的問題嗎?後端


我相信不少都遇到過這個問題。一旦有用戶在喊:網絡變慢了。第一件事情就是,netstat -a | grep TIME_WAIT | wc -l 一下。哎呀媽呀,幾千個TIME_WAIT.瀏覽器


而後,作的第一件事情就是:打開Google或者Bing,輸入關鍵詞:too many time wait。必定能找到解決方案,而排在最前面或者被不少人處處轉載的解決方案必定是:安全


打開 sysctl.conf 文件,修改如下幾個參數:
服務器


  • net.ipv4.tcp_tw_recycle = 1網絡

  • net.ipv4.tcp_tw_reuse = 1數據結構

  • net.ipv4.tcp_timestamps = 1


你也會被告知,開啓tw_recylce和tw_reuse必定須要timestamps的支持,並且這些配置通常不建議開啓,可是對解決TIME_WAIT不少的問題,有很好的用處。


接下來,你就直接修改了這幾個參數,reload一下,發現,咦,沒幾分鐘,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操做。


這裏又出現兩個問題:


  1. 上文提到的鏈接重用,那鏈接究竟是個什麼概念?

  2. 協議層爲何要設計一個TIME_WAIT狀態?這個狀態爲何默認等待2MSL時間纔會進入CLOSED


先解釋清楚這兩個問題,咱們再來看,開頭提到的幾個網絡配置究竟有什麼用,以及TIME_WAIT的後遺症問題。


Socket鏈接究竟是個什麼概念?


你們常常提socket,那麼,到底什麼是一個socket?其實,socket就是一個 五元組,包括:


  1. 源IP

  2. 源端口

  3. 目的IP

  4. 目的端口

  5. 類型:TCP or UDP


這個五元組,即標識了一條可用的鏈接。注意,有不少人把一個socket定義成四元組,也就是 源IP:源端口 + 目的IP:目的端口,這個定義是不正確的。


例如,若是你的本地出口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:43678, tcp, 180.97.33.108:80]


這條新的鏈接的源端口爲一個新的隨機端口 43678。


如此來看,若是你的本機須要壓測百度,那麼,你最多能夠建立多少個鏈接呢?我在文章《雲思路 | 輕鬆構建千萬級投票系統》裏也稍微提過這個問題,沒有閱讀過本文的,能夠發送「投票系統」閱讀。


第二個問題,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的數據包丟失,重傳第一次,沒有獲得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就依賴這些時間字段。


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. 客戶端IP地址爲:180.172.35.150,咱們能夠認爲是瀏覽器

  2. 負載均衡有兩個IP,外網IP地址爲 115.29.253.156,內網地址爲10.162.74.10;外網地址監聽80端口

  3. 負載均衡背後有兩臺Web服務器,一臺IP地址爲 10.162.74.43,監聽80端口;另外一臺爲 10.162.74.44,監聽 80 端口

  4. Web服務器會鏈接數據服務器,IP地址爲 10.162.74.45,監聽 3306 端口


這種簡單的架構下,咱們來看看,在不一樣的狀況下,咱們今天談論的tw_reuse/tw_recycle對網絡鏈接的影響。


先作個假定:

  1. 客戶端經過HTTP/1.1鏈接負載均衡,也就是說,HTTP協議投Connection爲keep-alive,因此咱們假定,客戶端 對 負載均衡服務器 的socket鏈接,客戶端會斷開鏈接,因此,TIME_WAIT出如今客戶端

  2. Web服務器和MySQL服務器的鏈接,咱們假定,Web服務器上的程序在鏈接結束的時候,調用close操做關閉socket資源鏈接,因此,TIME_WAIT出如今 Web 服務器端。


那麼,在這種假定下:

  1. Web服務器上,確定能夠配置開啓的配置:tcp_tw_reuse;若是Web服務器有不少連向DB服務器的鏈接,能夠保證socket鏈接的複用。

  2. 那麼,負載均衡服務器和Web服務器,誰先關閉鏈接,則決定了咱們怎麼配置tcp_tw_reuse/tcp_tw_recycle了


方案一:負載均衡服務器 首先關閉鏈接 


在這種狀況下,由於負載均衡服務器對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,也值得一讀。有了這些積累,可能就是一些實踐以及碎片化的學習和積累了。


寫在最後


這篇文章我斷斷續續寫了兩天,內容找了多個地方去驗證,包括看到Vincent Bernat的一篇文章以及Vincent在多個地方和別人的討論。期間,我也花了一些時間和Vincent探討了幾個我沒在tcp源碼裏翻找到的有疑問的地方。


我力求比散佈在網上的文章作到準確並儘可能整理的清晰一些。可是,也不免會

有疏漏或者有錯誤的地方,高手看到能夠隨時指正,並和我討論,你們一塊兒研究!

相關文章
相關標籤/搜索