TIME_WAIT

這個是高並發服務端常見的一個問題,一般的做法是修改sysctl的參數來解決。但是,做為一個有追求的程序猿,你需要多問幾個為什么,為什么會出現TIME_WAIT?出現這個合理嗎?

我們需要先回顧下tcp的知識,請看下面的狀態轉換圖(圖片來自「The TCP/IP Guide」):tcp

因為TCP連接是雙向的,所以在關閉連接的時候,兩個方向各自都需要關閉。先發FIN包的一方執行的是主動關閉;后發FIN包的一方執行的是被動關閉。主動關閉的一方會進入TIME_WAIT狀態,並且在此狀態停留兩倍的MSL時長。

修改sysctl的參數,只是控制TIME_WAIT的數量。你需要很明確的知道,在你的應用場景里面,你預期是服務端還是客戶端來主動關閉連接的。一般來說,都是客戶端來主動關閉的。

nginx在某些情況下,會主動關閉客戶端的請求,這個時候,返回值的connection為close。我們看兩個例子:

http 1.0協議

請求包:

GET /hello HTTP/1.0 User-Agent: curl/7.37.1 Host: 127.0.0.1 Accept: */* Accept-Encoding: deflate, gzip 

應答包:

HTTP/1.1 200 OK Date: Wed, 08 Jul 2015 02:53:54 GMT Content-Type: text/plain Connection: close Server: 360 web server hello world 

對於http 1.0協議,如果請求頭里面沒有包含connection,那么應答默認是返回Connection: close,也就是說nginx會主動關閉連接。

user agent

請求包:

POST /api/heartbeat.json HTTP/1.1 Content-Type: application/x-www-form-urlencoded Cache-Control: no-cache User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT) Accept-Encoding: gzip, deflate Accept: */* Connection: Keep-Alive Content-Length: 0 

應答包:

HTTP/1.1 200 OK Date: Mon, 06 Jul 2015 09:35:34 GMT Content-Type: text/plain Transfer-Encoding: chunked Connection: close Server: 360 web server Content-Encoding: gzip 

這個請求包是http1.1的協議,也聲明了Connection: Keep-Alive,為什么還會被nginx主動關閉呢?問題出在User-Agent,nginx認為終端的瀏覽器版本太低,不支持keep alive,所以直接close了。

在我們應用的場景下,終端不是通過瀏覽器而是后台請求的,而我們也沒法控制終端的User-Agent,那有什么方法不讓nginx主動去關閉連接呢?可以用keepalive_disable這個參數來解決。這個參數並不是字面的意思,用來關閉keepalive,而是用來定義哪些古代的瀏覽器不支持keepalive的,默認值是MSIE6。

keepalive_disable none; 

修改為none,就是認為不再通過User-Agent中的瀏覽器信息,來決定是否keepalive。

1、 time_wait的作用:

復制代碼
TIME_WAIT狀態存在的理由:
1)可靠地實現TCP全雙工連接的終止
   在進行關閉連接四次揮手協議時,最后的ACK是由主動關閉端發出的,如果這個最終的ACK丟失,服務器將重發最終的FIN,
因此客戶端必須維護狀態信息允許它重發最終的ACK。如果不維持這個狀態信息,那么客戶端將響應RST分節,服務器將此分節解釋成一個錯誤(在java中會拋出connection reset的SocketException)。
因而,要實現TCP全雙工連接的正常終止,必須處理終止序列四個分節中任何一個分節的丟失情況,主動關閉的客戶端必須維持狀態信息進入TIME_WAIT狀態。
 
2)允許老的重復分節在網絡中消逝 
TCP分節可能由於路由器異常而“迷途”,在迷途期間,TCP發送端可能因確認超時而重發這個分節,迷途的分節在路由器修復后也會被送到最終目的地,這個原來的迷途分節就稱為lost duplicate。
在關閉一個TCP連接后,馬上又重新建立起一個相同的IP地址和端口之間的TCP連接,后一個連接被稱為前一個連接的化身(incarnation),那么有可能出現這種情況,前一個連接的迷途重復分組在前一個連接終止后出現,從而被誤解成從屬於新的化身。
為了避免這個情況,TCP不允許處於TIME_WAIT狀態的連接啟動一個新的化身,因為TIME_WAIT狀態持續2MSL,就可以保證當成功建立一個TCP連接的時候,來自連接先前化身的重復分組已經在網絡中消逝。
復制代碼

2、大量TIME_WAIT造成的影響:

      在高並發短連接的TCP服務器上,當服務器處理完請求后立刻主動正常關閉連接。這個場景下會出現大量socket處於TIME_WAIT狀態。如果客戶端的並發量持續很高,此時部分客戶端就會顯示連接不上。
我來解釋下這個場景。主動正常關閉TCP連接,都會出現TIMEWAIT。

為什么我們要關注這個高並發短連接呢?有兩個方面需要注意:
1. 高並發可以讓服務器在短時間范圍內同時占用大量端口,而端口有個0~65535的范圍,並不是很多,刨除系統和其他服務要用的,剩下的就更少了。
2. 在這個場景中,短連接表示“業務處理+傳輸數據的時間 遠遠小於 TIMEWAIT超時的時間”的連接

      這里有個相對長短的概念,比如取一個web頁面,1秒鍾的http短連接處理完業務,在關閉連接之后,這個業務用過的端口會停留在TIMEWAIT狀態幾分鍾,而這幾分鍾,其他HTTP請求來臨的時候是無法占用此端口的(占着茅坑不拉翔)。單用這個業務計算服務器的利用率會發現,服務器干正經事的時間和端口(資源)被掛着無法被使用的時間的比例是 1:幾百,服務器資源嚴重浪費。(說個題外話,從這個意義出發來考慮服務器性能調優的話,長連接業務的服務就不需要考慮TIMEWAIT狀態。同時,假如你對服務器業務場景非常熟悉,你會發現,在實際業務場景中,一般長連接對應的業務的並發量並不會很高
     綜合這兩個方面,持續的到達一定量的高並發短連接,會使服務器因端口資源不足而拒絕為一部分客戶服務。同時,這些端口都是服務器臨時分配,無法用SO_REUSEADDR選項解決這個問題。

關於time_wait的反思

存在即是合理的,既然TCP協議能盛行四十多年,就證明他的設計合理性。所以我們盡可能的使用其原本功能。
依靠TIME_WAIT狀態來保證我的服務器程序健壯,服務功能正常。
那是不是就不要性能了呢?並不是。如果服務器上跑的短連接業務量到了我真的必須處理這個TIMEWAIT狀態過多的問題的時候,我的原則是盡量處理,而不是跟TIMEWAIT干上,非先除之而后快。
如果盡量處理了,還是解決不了問題,仍然拒絕服務部分請求,那我會采取負載均衡來抗這些高並發的短請求。持續十萬並發的短連接請求,兩台機器,每台5萬個,應該夠用了吧。一般的業務量以及國內大部分網站其實並不需要關注這個問題,一句話,達不到時才需要關注這個問題的訪問量。

小知識點:

TCP協議發表:1974年12月,卡恩、瑟夫的第一份TCP協議詳細說明正式發表。當時美國國防部與三個科學家小組簽定了完成TCP/IP的協議,結果由瑟夫領銜的小組捷足先登,首先制定出了通過詳細定義的TCP/IP協議標准。當時作了一個試驗,將信息包通過點對點的衛星網絡,再通過陸地電纜
,再通過衛星網絡,再由地面傳輸,貫串歐洲和美國,經過各種電腦系統,全程9.4萬公里竟然沒有丟失一個數據位,遠距離的可靠數據傳輸證明了TCP/IP協議的成功。

 

3、案列分析:

    首先,根據一個查詢TCP連接數,來說明這個問題。

復制代碼
netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'
LAST_ACK 14
SYN_RECV 348
ESTABLISHED 70
FIN_WAIT1 229
FIN_WAIT2 30
CLOSING 33
TIME_WAIT 18122
復制代碼

狀態描述:

  View Code

命令解釋:

  View Code

 

如何盡量處理TIMEWAIT過多?

編輯內核文件/etc/sysctl.conf,加入以下內容:

net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;
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 時間

然后執行 /sbin/sysctl -p 讓參數生效.

/etc/sysctl.conf是一個允許改變正在運行中的Linux系統的接口,它包含一些TCP/IP堆棧和虛擬內存系統的高級選項,修改內核參數永久生效。

簡單來說,就是打開系統的TIMEWAIT重用和快速回收。

如果以上配置調優后性能還不理想,可繼續修改一下配置:

復制代碼
vi /etc/sysctl.conf
net.ipv4.tcp_keepalive_time = 1200 
#表示當keepalive起用的時候,TCP發送keepalive消息的頻度。缺省是2小時,改為20分鍾。
net.ipv4.ip_local_port_range = 1024 65000 
#表示用於向外連接的端口范圍。缺省情況下很小:32768到61000,改為1024到65000。
net.ipv4.tcp_max_syn_backlog = 8192 
#表示SYN隊列的長度,默認為1024,加大隊列長度為8192,可以容納更多等待連接的網絡連接數。
net.ipv4.tcp_max_tw_buckets = 5000 
#表示系統同時保持TIME_WAIT套接字的最大數量,如果超過這個數字,TIME_WAIT套接字將立刻被清除並打印警告信息。
默認為180000,改為5000。對於Apache、Nginx等服務器,上幾行的參數可以很好地減少TIME_WAIT套接字數量,但是對於 Squid,效果卻不大。此項參數可以控制TIME_WAIT套接字的最大數量,避免Squid服務器被大量的TIME_WAIT套接字拖死。