1. 程式人生 > >從問題看本質: 研究TCP close_wait的內幕

從問題看本質: 研究TCP close_wait的內幕

最近遇到的一個關於socket.close的問題,在某個應用伺服器出現的狀況(執行netstat -np | grep tcp): 

tcp        0      0 10.224.122.16:50158         10.224.112.58:8788          CLOSE_WAIT

tcp        0      0 10.224.122.16:37655         10.224.112.58:8788          CLOSE_WAIT

tcp        1      0 127.0.0.1:32713             127.0.0.1:8080              CLOSE_WAIT

tcp       38      0 10.224.122.16:34538         10.224.125.42:443           CLOSE_WAIT

tcp       38      0 10.224.122.16:33394         10.224.125.42:443           CLOSE_WAIT

tcp        1      0 10.224.122.16:18882         10.224.125.10:80            CLOSE_WAIT

tcp        1      0 10.224.122.16:18637         10.224.125.10:80            CLOSE_WAIT

tcp        1      0 10.224.122.16:19655         10.224.125.12:80            CLOSE_WAIT

........................................

總共出現了200個CLOSE_WAIT的socket.而且這些socket長時間得不到釋放.下面我們來看看為什麼會出現這種大量socket的CLOSE_WAIT情況

首先我們要搞清楚的是,這個socket是誰發起的,我們可以看到122.16這臺機器開了很多埠,而且埠號都很大,125.12 或者125.10上的埠都是很常見伺服器埠,所以122.16上這麼多CLOSE_WAIT

的socket是由122.16開啟的,換句話說這臺機器是傳統的客戶端,它會主動的請求其他機器的服務埠.

要搞清楚為什麼會出現CLOSE_WAIT,那麼首先我們必須要清楚CLOSE_WAIT的機制和原理.

假設我們有一個client, 一個server.

當client主動發起一個socket.close()這個時候對應TCP來說,會發生什麼事情呢?如下圖所示.

client首先發送一個FIN訊號給server, 這個時候client變成了FIN_WAIT_1的狀態, server端收到FIN之後,返回ACK,然後server端的狀態變成了CLOSE_WAIT.

接著server端需要傳送一個FIN給client,然後server端的狀態變成了LAST_ACK,接著client返回一個ACK,然後server端的socket就被成功的關閉了.

從這裡可以看到,如果由客戶端主動關閉一連結,那麼客戶端是不會出現CLOSE_WAIT狀態的.客戶端主動關閉連結,那麼Server端將會出現CLOSE_WAIT的狀態.

而我們的伺服器上,是客戶端socket出現了CLOSE_WAIT,由此可見這個是由於server主動關閉了server上的socket.

那麼當server主動發起一個socket.close(),這個時候又發生了一些什麼事情呢.

從圖中我們可以看到,如果是server主動關閉連結,那麼Client則有可能進入CLOSE_WAIT,如果Client不傳送FIN包,那麼client就一直會處在CLOSE_WAIT狀態(後面我們可以看到有引數可以調整這個時間).

那麼現在我們要搞清楚的是,在第二中場景中,為什麼Client不傳送FIN包給server.要搞清楚這個問題,我們首先要搞清楚server是怎麼發FIN包給client的,其實server就是呼叫了

socket.close方法而已,也就是說如果要client傳送FIN包,那麼client就必須呼叫socket.close,否則就client就一直會處在CLOSE_WAIT(但事實上不同作業系統這點的實現還不一樣,

在ahuaxuan(ahuaxuan.iteye.com)的例子中也出現了這樣的case).

下面我們來做幾個實驗

實驗一:

環境:

伺服器端:win7+tomcat,tomcat的keep-alive的時間為預設的15s.

客戶端:mac os

實驗步驟:伺服器啟動後,客戶端向伺服器傳送一個get請求,然後客戶端阻塞,等待伺服器端的socket超時.通過netstat -np tcp可以看到的情況是傳送get請求時,伺服器和客戶端連結是ESTABLISHED, 15s之後,客戶端變成了CLOSE_WAIT,而伺服器端變成了FIN_WAIT_2.這一點也在我們的預料之中,而這個時候由於客戶端執行緒阻塞,客戶端socket空置在那裡,不做任何操作,2分鐘過後,這個連結不管是在win7上,還是在mac os都看不到了.可見,FIN_WAIT_2或者CLOSE_WAIT有一個timeout.在後面的實驗,可以證明,在這個例子中,其實是FIN_WAIT_2有一個超時,一旦過了2分鐘,那麼win7會發一個RST給mac os要求關閉雙方的socket.

實驗二

伺服器端:ubuntu9.10+tomcat,tomcat的keep-alive的時間為預設的15s.

客戶端:mac os

實驗步驟:伺服器啟動後,客戶端向伺服器傳送一個get請求,然後客戶端阻塞,等待伺服器端的socket超時.通過netstat -np tcp(ubuntu使用netstat -np|grep tcp)可以看到的情況是傳送get請求時,伺服器和客戶端連結是ESTABLISHED, 15s之後,客戶端變成了CLOSE_WAIT,而伺服器端變成了FIN_WAIT_2.這一點也也在我們的預料之中,而這個時候由於客戶端執行緒阻塞,客戶端socket空置在那裡,不做任何操作,1分鐘過後,ubuntu上的那個socket不見了,但是mac os上的socket還在,而且還是CLOSE_WAIT,這說明,FIN_WAIT_2確實有一個超時時間,win7上的超時操作可以關閉mac os上的socket,而ubuntu上的FIN_WAIT_2超時操作卻不能關閉mac os上的socket(其狀一直是CLOSE_WAIT).

實驗三

伺服器端:mac os+tomcat,tomcat的keep-alive的時間為預設的15s.

客戶端:mac os

實驗步驟:伺服器啟動後,客戶端向伺服器傳送一個get請求,然後客戶端阻塞,等待伺服器端的socket超時.通過netstat -np tcp可以看到的情況是傳送get請求時,伺服器和客戶端連結是ESTABLISHED, 15s之後,客戶端變成了CLOSE_WAIT,而伺服器端變成了FIN_WAIT_2.這一點也在我們的預料之中,而這個時候由於客戶端執行緒阻塞,客戶端socket空置在那裡,不做任何操作,4分鐘過後,mac os伺服器端上的那個socket不見了,但是mac os客戶端上的socket還在,而且還是CLOSE_WAIT,這說明,FIN_WAIT_2確實有一個超時時間,win7上的超時操作可以關閉mac os上的socket,而ubuntu和mac os上的FIN_WAIT_2超時操作卻不能關閉mac os上的socket.

總結, 當伺服器的核心不一樣上FIN_WAIT_2的超時時間和操作是不一樣的.

經查:控制FIN_WAIT_2的引數為:

/proc/sys/net/ipv4/tcp_fin_timeout

如 果套接字由本端要求關閉,這個引數決定了它保持在FIN-WAIT-2狀態的時間。對端可以出錯並永遠不關閉連線,甚至意外當機。預設值是60秒。2.2 核心的通常值是180秒,你可以按這個設定,但要記住的是,即使你的機器是一個輕載的WEB伺服器,也有因為大量的死套接字而記憶體溢位的風險,FIN- WAIT-2的危險性比FIN-WAIT-1要小,因為它最多隻能吃掉1.5K記憶體,但是它們的生存期長些。參見tcp_max_orphans。

實驗四

伺服器端:ubuntu9.10+tomcat,tomcat的keep-alive的時間為預設的15s.

客戶端:mac os

實驗步驟:伺服器啟動後,客戶端向伺服器傳送一個get請求,然後關閉客戶端關閉socket.通過netstat -np tcp可以看到的情況是傳送get請求時,伺服器和客戶端連結是ESTABLISHED, 客戶端拿到資料之後,客戶端變成了TIME_WAIT,而伺服器端變成了已經看不到這個socket了.這一點也也在我們的預料之中,誰主動關閉連結,那麼誰就需要進入TIME_WAIT狀態(除非他的FIN_WAIT_2超時了),大約1分鐘之後這個socket在客戶端也消失了.

實驗證明TIME_WAIT的狀態會存在一段時間,而且在這個時間端裡,這個FD是不能被回收的.

但是我們的問題是客戶端有很多CLOSE_WAIT,而且我們的伺服器不是windows,而是linux,所以CLOSE_WAIT有沒有超時時間呢,肯定有,而且預設情況下這個超時時間應該是比較大的.否則不會一下子看到兩百個CLOSE_WAIT的狀態.

客戶端解決方案:

1.由於socket.close()會導致FIN訊號,而client的socket CLOSE_WAIT就是因為該socket該關的時候,我們沒有關,所以我們需要一個執行緒池來檢查空閒連線中哪些進入了超時狀態(idleTIME),但進入超時

的socket未必是CLOSE_WAIT的狀態的.不過如果我們把空閒超時的socket關閉,那麼CLOSE_WAIT的狀態就會消失.(問題:像HttpClient這樣的工具包中,如果要檢查連結池,那麼則需要鎖定整個池,而這個時候,使用者請求獲取connection的操作只能等待,在高併發的時候會造成程式響應速度下降,具體參考IdleConnectionTimeoutThread.java(HttpClient3.1))

2.經查,其實有引數可以調整CLOSE_WAIT的持續時間,如果我們改變這個時間,那麼可以讓CLOSE_WAIT只保持很短的時間(當然這個引數不只作用在CLOSE_WAIT上,縮短這個時間可能會帶來其他的影響).在客戶端機器上修改如下:

sysctl -w net.ipv4.tcp_keepalive_time=60(預設是2小時,現在改成了60秒)

sysctl -w net.ipv4.tcp_keepalive_probes=2

sysctl -w net.ipv4.tcp_keepalive_intvl=2

我們將CLOSE_WAIT的檢查時間設定為30s,這樣一個CLOSE_WAIT只會存在30S.

3. 當然,最重要的是我們要檢查客戶端連結的空閒時間,空閒時間可以由客戶端自行定義,比如idleTimeout,也可由伺服器來決定,伺服器只需要每次在response.header中加入一個頭資訊,比如說名字叫做timeout頭,當然一般情況下我們會用keep-alive這個頭欄位, 如果伺服器設定了該欄位,那麼客戶端拿到這個屬性之後,就知道自己的connection最大的空閒時間,這樣不會由於伺服器關閉socket,而導致客戶端socket一直close_wait在那裡.

伺服器端解決方案

4.前面講到客戶端出現CLOSE_WAIT是由於伺服器端Socket的讀超時,也是TOMCAT中的keep-alive引數.那麼如果我們把這個超時時間設定的長點,會有什麼影響?

如果我們的tomcat既服務於瀏覽器,又服務於其他的APP,而且我們把connection的keep-alive時間設定為10分鐘,那麼帶來的後果是瀏覽器開啟一個頁面,然後這個頁面一直不關閉,那麼伺服器上的socket也不能關閉,它所佔用的FD也不能服務於其他請求.如果併發一高,很快伺服器的資源將會被耗盡.新的請求再也進不來. 那麼如果把keep-alive的時間設定的短一點呢,比如15s? 那麼其他的APP來訪問這個伺服器的時候,一旦這個socket, 15s之內沒有新的請求,那麼客戶端APP的socket將出現大量的CLOSE_WAIT狀態.

所以如果出現這種情況,建議將你的server分開部署,服務於browser的部署到單獨的JVM例項上,保持keep-alive為15s,而服務於架構中其他應用的功能部署到另外的JVM例項中,並且將keep-alive的時間設定的更

長,比如說1個小時.這樣客戶端APP建立的connection,如果在一個小時之內都沒有重用這條connection,那麼客戶端的socket才會進入CLOSE_WAIT的狀態.針對不同的應用場景來設定不同的keep-alive時間,可以幫助我們提高程式的效能.

5.如果我們的應用既服務於瀏覽器,又服務於其他的APP,那麼我們還有一個終極解決方案.

那就是配置多個connector, 如下:

<!-- for browser -->

 <Connector port="8080" protocol="HTTP/1.1" 

               connectionTimeout="20000" 

               redirectPort="8443" />

<!-- for other APP -->

<Connector port="8081" protocol="HTTP/1.1" 

               connectionTimeout="20000" 

               redirectPort="8443" keepAliveTimeout="330000" />

訪問的時候,瀏覽器使用8080埠,其他的APP使用8081埠.這樣可以保證瀏覽器請求的socket在15s之內如果沒有再次使用,那麼tomcat會主動關閉該socket,而其他APP請求的socket在330s之內沒有使用,才關閉該socket,這樣做可以大大減少其他APP上出現CLOSE_WAIT的機率.

你一定會問,如果我不設定keepAliveTimeout又怎麼樣呢,反正客戶端有idleTimeout,客戶端的close_wait不會持續太長時間,請注意看上圖中標紅的地方,一個是close_wait,還有一個是time_wait狀態,也就是說誰主動發起請求,那麼它將會最終進入time_wait狀態,據說windows上這個time_wait將持續4分鐘,我在linux上的測試表明,linux上它大概是60s左右,也就是說高併發下,也就是伺服器也需要過60s左右才能真正的釋放這個FD.所以我們如果提供http服務給其他APP,那麼我們最好讓客戶端優先關閉socket,也就是將客戶端的idleTimeout設定的比server的keepalivetimeout小一點.這樣保證time_wait出現在客戶端. 而不是資源較為緊張的伺服器端.

總結:

       本文中ahuaxuan給大家揭示了TCP層client和server端socket關閉的一般流程,並且指出異常情況下client和server端各自會發生的情況,包含了在不同平臺上出現了的不同情況, 同時說明了在應用層上我們可以做什麼樣的邏輯來保證socket關閉時對server端帶來最小的影響.

下面是網上找到的一些資料:

 寫道


/proc/sys/net/ipv4/tcp_keepalive_time
當keepalive起用的時候,TCP傳送keepalive訊息的頻度。預設是2小時。

/proc/sys/net/ipv4/tcp_keepalive_intvl
當探測沒有確認時,重新發送探測的頻度。預設是75秒。

/proc/sys/net/ipv4/tcp_keepalive_probes
在認定連線失效之前,傳送多少個TCP的keepalive探測包。預設值是9。這個值乘以tcp_keepalive_intvl之後決定了,一個連線傳送了keepalive之後可以有多少時間沒有迴應。


/proc/sys/net/ipv4/tcp_max_orphans
系 統中最多有多少個TCP套接字不被關聯到任何一個使用者檔案控制代碼上。如果超過這個數字,孤兒連線將即刻被複位並打印出警告資訊。這個限制僅僅是為了防止簡單的DoS攻擊,你絕對不能過分依靠它或者人為地減小這個值,更應該增加這個值(如果增加了記憶體之後)。This limit exists only to prevent simple DoS attacks, you _must_ not rely on this or lower the limit artificially, but rather increase it (probably, after increasing installed memory), if network conditions require more than default value, and tune network services to linger and kill such states more aggressively. 讓我再次提醒你:每個孤兒套接字最多能夠吃掉你64K不可交換的記憶體。

/proc/sys/net/ipv4/tcp_orphan_retries
本端試圖關閉TCP連線之前重試多少次。預設值是7,相當於50秒~16分鐘(取決於RTO)。如果你的機器是一個過載的WEB伺服器,你應該考慮減低這個值,因為這樣的套接字會消耗很多重要的資源。參見tcp_max_orphans。

/proc/sys/net/ipv4/tcp_max_syn_backlog
記 錄的那些尚未收到客戶端確認資訊的連線請求的最大值。對於有128M記憶體的系統而言,預設值是1024,小記憶體的系統則是128。如果伺服器不堪重負,試 試提高這個值。注意!如果你設定這個值大於1024,最好同時調整include/net/tcp.h中的TCP_SYNQ_HSIZE,以保證 TCP_SYNQ_HSIZE*16 ≤tcp_max_syn_backlo,然後重新編譯核心。

/proc/sys/net/ipv4/tcp_max_tw_buckets
系 統同時保持timewait套接字的最大數量。如果超過這個數字,time-wait套接字將立刻被清除並列印警告資訊。這個限制僅僅是為了防止簡單的 DoS攻擊,你絕對不能過分依靠它或者人為地減小這個值,如果網路實際需要大於預設值,更應該增加這個值(如果增加了記憶體之後)。