linux服務器大量TIME_WAIT狀態問題

上篇:問題與理論

最近遇到一個線上報警:服務器出現大量TIME_WAIT致使其沒法與下游模塊創建新HTTP鏈接,在解決過程當中,經過查閱經典教材和技術文章,加深了對TCP網絡問題的理解。做爲筆記,記錄於此。
        備註:本文主要介紹TCP編程中涉及到的衆多基礎知識,關於實際工程中對由TIME_WAIT引起的不能創建新鏈接問題的解決方法將在下篇筆記中給出。html

1. 實際問題
        初步查看發現,沒法對外新建TCP鏈接時,線上服務器存在大量處於TIME_WAIT狀態的TCP鏈接(最多的一次爲單機10w+,其中引發報警的那個模塊產生的TIME_WAIT約2w),致使其沒法跟下游模塊創建新TCP鏈接。
        TIME_WAIT涉及到TCP釋放鏈接過程當中的狀態遷移,也涉及到具體的socket api對TCP狀態的影響,下面開始逐步介紹這些概念。linux

2. TCP狀態遷移
       面向鏈接的TCP協議要求每次peer間通訊前創建一條TCP鏈接,該鏈接可抽象爲一個4元組(four-tuple,有時也稱socket pair):(local_ip, local_port, remote_ip,remote_port),這4個元素惟一地表明一條TCP鏈接。
       1)TCP Connection Establishment
       TCP創建鏈接的過程,一般又叫「三次握手」(three-way handshake),可用下圖來示意:
       web

      可對上圖作以下解釋:
        a. client向server發送SYN並約定初始包序號(sequence number)爲J;
        b. server發送本身的SYN並代表初始包序號爲K,同時,針對client的SYNJ返回ACKJ+1(注:J+1表示server指望的來自該client的下一個包序爲J+1);
        c. client收到來自server的SYN+ACK後,發送ACKK+1,至此,TCP創建成功。
        其實,在TCP創建時的3次握手過程當中,還要經過SYN包商定各自的MSS,timestamp等參數,這涉及到協議的細節,本文旨在拋磚引玉,再也不展開。shell

           2)TCPConnection Termination
       與創建鏈接的3次握手相對應,釋放一條TCP鏈接時,須要通過四步交互(又稱「四次揮手」),以下圖所示:
        
         可對上圖作以下解釋:
       a. 鏈接的某一方先調用close()發起主動關閉(active close),該api會促使TCP傳輸層向remotepeer發送FIN包,該包代表發起active close的application再也不發送數據(特別注意:這裏「再也不發送數據」的承諾是從應用層角度來看的,在TCP傳輸層,仍是要將該application對應的內核tcp send buffer中當前還沒有發出的數據發到鏈路上)。                
       remote peer收到FIN後,須要完成被動關閉(passive close),具體分爲兩步:
       b. 首先,在TCP傳輸層,先針對對方的FIN包發出ACK包(主要ACK的包序是在對方FIN包序基礎上加1);
       c. 接着,應用層的application收到對方的EOF(end-of-file,對方的FIN包做爲EOF傳給應用層的application)後,得知這條鏈接不會再有來自對方的數據,因而也調用close()關閉鏈接,該close會促使TCP傳輸層發送FIN。
       d. 發起主動關閉的peer收到remote peer的FIN後,發送ACK包,至此,TCP鏈接關閉。
       注意1:TCP鏈接的任一方都可以首先調用close()以發起主動關閉,上圖以client主動發起關閉作說明,而不是說只能client發起主動關閉。
       注意2:上面給出的TCP創建/釋放鏈接的過程描述中,未考慮因爲各類緣由引發的重傳、擁塞控制等協議細節,感興趣的同窗能夠查看各類TCP RFC Documents ,好比TCP RFC793編程

        3)TCP StateTransition Diagram
       上面介紹了TCP創建、釋放鏈接的過程,此處對TCP狀態機的遷移過程作整體說明。將TCP RFC793中描述的TCP狀態機遷移圖摘出以下(下圖引用自這裏):
     
          TCP狀態機共含11個狀態,狀態間在各類socket apis的驅動下進行遷移,雖然此圖看起來錯綜複雜,但對於有必定TCP網絡編程經驗的同窗來講,理解起來仍是比較容易的。限於篇幅,本文不許備展開詳述,想了解具體遷移過程的新手同窗,建議閱讀《Linux Network Programming Volume1》第2.6節。api

 

3. TIME_WAIT狀態
        
通過前面的鋪墊,終於要講到與本文主題相關的內容了。 ^_^
        從TCP狀態遷移圖可知,只有首先調用close()發起主動關閉的一方纔會進入TIME_WAIT狀態,並且是必須進入(圖中左下角所示的3條狀態遷移線最終均要進入該狀態才能回到初始的CLOSED狀態)。
        從圖中還可看到,進入TIME_WAIT狀態的TCP鏈接須要通過2MSL才能回到初始狀態,其中,MSL是指Max
Segment Lifetime,即數據包在網絡中的最大生存時間。每種TCP協議的實現方法均要指定一個合適的MSL值,如RFC1122給出的建議值爲2分鐘,又如Berkeley體系的TCP實現一般選擇30秒做爲MSL值。這意味着TIME_WAIT的典型持續時間爲1-4分鐘。
       TIME_WAIT狀態存在的緣由主要有兩點:
    
   1)爲實現TCP這種全雙工(full-duplex)鏈接的可靠釋放
       參考本文前面給出的TCP釋放鏈接4次揮手示意圖,假設發起active close的一方(圖中爲client)發送的ACK(4次交互的最後一個包)在網絡中丟失,那麼因爲TCP的重傳機制,執行passiveclose的一方(圖中爲server)須要重發其FIN,在該FIN到達client(client是active close發起方)以前,client必須維護這條鏈接的狀態(儘管它已調用過close),具體而言,就是這條TCP鏈接對應的(local_ip, local_port)資源不能被當即釋放或從新分配。直到romete peer重發的FIN達到,client也重發ACK後,該TCP鏈接才能恢復初始的CLOSED狀態。若是activeclose方不進入TIME_WAIT以維護其鏈接狀態,則當passive close方重發的FIN達到時,active close方的TCP傳輸層會以RST包響應對方,這會被對方認爲有錯誤發生(而事實上,這是正常的關閉鏈接過程,並不是異常)。
        2)爲使舊的數據包在網絡因過時而消失
       爲說明這個問題,咱們先假設TCP協議中不存在TIME_WAIT狀態的限制,再假設當前有一條TCP鏈接:(local_ip, local_port, remote_ip,remote_port),因某些緣由,咱們先關閉,接着很快以相同的四元組創建一條新鏈接。本文前面介紹過,TCP鏈接由四元組惟一標識,所以,在咱們假設的狀況中,TCP協議棧是沒法區分先後兩條TCP鏈接的不一樣的,在它看來,這根本就是同一條鏈接,中間先釋放再創建的過程對其來講是「感知」不到的。這樣就可能發生這樣的狀況:前一條TCP鏈接由local peer發送的數據到達remote peer後,會被該remot peer的TCP傳輸層當作當前TCP鏈接的正常數據接收並向上傳遞至應用層(而事實上,在咱們假設的場景下,這些舊數據到達remote peer前,舊鏈接已斷開且一條由相同四元組構成的新TCP鏈接已創建,所以,這些舊數據是不該該被向上傳遞至應用層的),從而引發數據錯亂進而致使各類沒法預知的詭異現象。做爲一種可靠的傳輸協議,TCP必須在協議層面考慮並避免這種狀況的發生,這正是TIME_WAIT狀態存在的第2個緣由。
       具體而言,local peer主動調用close後,此時的TCP鏈接進入TIME_WAIT狀態,處於該狀態下的TCP鏈接不能當即以一樣的四元組創建新鏈接,即發起active close的那方佔用的local port在TIME_WAIT期間不能再被從新分配。因爲TIME_WAIT狀態持續時間爲2MSL,這樣保證了舊TCP鏈接雙工鏈路中的舊數據包均因過時(超過MSL)而消失,此後,就能夠用相同的四元組創建一條新鏈接而不會發生先後兩次鏈接數據錯亂的狀況。安全

 4. socket api: close() 和 shutdown()
       
由前面內容可知,對一條TCP鏈接而言,首先調用close()的一方會進入TIME_WAIT狀態,除此以外,關於close()還有一些細節須要說明。
       對一個tcp socket調用close()的默認動做是將該socket標記爲已關閉並當即返回到調用該api進程中。此時,從應用層來看,該socket fd不能再被進程使用,即不能再做爲read或write的參數。而從傳輸層來看,TCP會嘗試將目前send buffer中積壓的數據發到鏈路上,而後纔會發起TCP的4次揮手以完全關閉TCP鏈接。
       調用close()是關閉TCP鏈接的正常方式,但這種方式存在兩個限制,而這正是引入shutdown()的緣由:
       1)close()其實只是將socket fd的引用計數減1,只有當該socket fd的引用計數減至0時,TCP傳輸層纔會發起4次握手從而真正關閉鏈接。而shutdown則能夠直接發起關閉鏈接所需的4次握手,而不用受到引用計數的限制;
       2)close()會終止TCP的雙工鏈路。因爲TCP鏈接的全雙工特性,可能會存在這樣的應用場景:local peer不會再向remote peer發送數據,而remote peer可能還有數據須要發送過來,在這種狀況下,若是local peer想要通知remote peer本身不會再發送數據但還會繼續收數據這個事實,用close()是不行的,而shutdown()能夠完成這個任務。服務器

       close()和shutdown()的具體調用方法能夠man查看,此處再也不贅述。網絡

       以上就是本文要分析和解決的「因爲TIME_WAIT太多致使沒法對外創建新鏈接」問題所須要掌握的基礎知識。下一篇筆記會在本文基礎上介紹這個問題具體的解決方法。^_^併發

 

 

 

 

 

 

下篇:問題分析與實戰

 

 上篇筆記主要介紹了與TIME_WAIT相關的基礎知識,本文則從實踐出發,說明如何解決文章標題提出的問題。

1. 查看系統網絡配置和當前TCP狀態
        在定位並處理應用程序出現的網絡問題時,瞭解系統默認網絡配置是很是必要的。以x86_64平臺Linux kernelversion 2.6.9的機器爲例,ipv4網絡協議的默認配置能夠在/proc/sys/net/ipv4/下查看,其中與TCP協議棧相關的配置項均以tcp_xxx命名,關於這些配置項的含義,請參考這裏的文檔,此外,還能夠查看linux源碼樹中提供的官方文檔(src/linux/Documentation/ip-sysctl.txt)。下面列出我機器上幾個需重點關注的配置項及其默認值:

cat /proc/sys/net/ipv4/ip_local_port_range      32768   61000
cat /proc/sys/net/ipv4/tcp_max_syn_backlog      1024
cat /proc/sys/net/ipv4/tcp_syn_retries          5
cat /proc/sys/net/ipv4/tcp_max_tw_buckets       180000
cat /proc/sys/net/ipv4/tcp_tw_recycle           0
cat /proc/sys/net/ipv4/tcp_tw_reuse             0

        其中,前3項分別說明了local port的分配範圍(默認的可用端口數不到3w)、incomplete connection queue的最大長度以及3次握手時SYN的最大重試次數,這3項配置的含義,有個概念便可。後3項配置的含義則須要理解,由於它們在定位、解決問題過程當中要用到,下面進行重點說明。
        1) tcp_max_tw_buckets
         這篇文檔 是這樣描述的:Maximal number of time wait sockets held by system simultaneously. If this number is exceeded TIME_WAIT socket is immediately destroyed and warning is printed. This limit exists only to prevent simple DoS attacks, you must not lower the limit artificially, but rather increase it (probably, after increasing installed memory), if network conditions require more than default value (180000).
        可見,該配置項用來防範簡單的DoS攻擊 ,在某些狀況下,能夠適當調大,但絕對不該調小,不然,後果自負。。。
         2) tcp_tw_recycle
        Enable fast recycling of sockets in TIME-WAIT status. The defaultvalue is 0 (disabled). It should not be changed without advice/request of technical experts.
        該配置項可用於快速回收處於TIME_WAIT狀態的socket以便從新分配。默認是關閉的,必要時能夠開啓該配置。可是開啓該配置項後,有一些須要注意的地方,本文後面會提到。
         3) tcp_tw_reuse
         Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. The default value is 0. It should not
be changed without advice/request of technical experts.
        開啓該選項後,kernel會複用處於TIME_WAIT狀態的socket,固然複用的前提是「從協議角度來看,複用是安全的」。關於「 在什麼狀況下,協議認爲複用是安全的 」這個問題,這篇文章 從linux kernel源碼中挖出了答案,感興趣的同窗能夠查看。

 2. 網絡問題定位思路
        參考前篇筆記開始處描述的線上實際問題,收到某臺機器沒法對外創建新鏈接的報警時,排查定位問題過程以下:
       用netstat –at | grep 「TIME_WAIT」統計發現,當時出問題的那臺機器上共有10w+處於TIME_WAIT狀態的TCP鏈接,進一步分析發現,由報警模塊引發的TIME_WAIT鏈接有2w+。將netstat輸出的統計結果重定位到文件中繼續分析,通常會看到本機的port被大量佔用。
        由本文前面介紹的系統配置項可知,tcp_max_tw_buckets默認值爲18w,而ip_local_port_range範圍不到3w,大量的TIME_WAIT狀態使得local port在TIME_WAIT持續期間不能被再次分配,即沒有可用的local port,這將是致使新建鏈接失敗的最大緣由
        在這裏提醒你們:上面的結論只是咱們的初步判斷,具體緣由還須要根據代碼的異常返回值(如socket api的返回值及errno等)和模塊日誌作進一步確認。沒法創建新鏈接的緣由還多是被其它模塊列入黑名單了,本人就有過這方面的教訓:程序中用libcurl api請求下游模塊失敗,初步定位發現機器TIME_WAIT狀態不少,因而沒仔細分析curl輸出日誌就認爲是TIME_WAIT引發的問題,致使浪費了不少時間,折騰了半天發現不對勁後纔想起,下游模塊有防攻擊機制,而發起請求的機器ip被不在下游模塊的訪問白名單內,高峯期上游模塊經過curl請求下游的次數太過頻繁被列入黑名單,新建鏈接時被下游模塊的TCP層直接以RST包斷開鏈接,致使curl api返回」Recv failure: Connection reset by peer」的錯誤。慘痛的教訓呀 =_=
       另外,關於什麼時候發送RST包,《Unix Network Programming Volume 1》第4.3節作了說明,做爲筆記,摘出以下:
       An RST is a type of TCP segment that is sent by TCP when somethingis wrong.Three conditions that generatean RST are:            
        1) when a SYN arrives for a port that has no listening server;
        2) when TCP wants to abort an existing connection;
        3) when TCP receives a segment for a connection that does not exist. (TCPv1 [pp.246–250] contains additional information.)

3. 解決方法
        能夠用兩種思路來解決機器TIME_WAIT過多致使沒法對外創建新TCP鏈接的問題。
        3.1 修改系統配置
        具體來講,須要修改本文前面介紹的tcp_max_tw_buckets、tcp_tw_recycle、tcp_tw_reuse這三個配置項。
        1)將tcp_max_tw_buckets調大,從本文第一部分可知,其默認值爲18w(不一樣內核可能有所不一樣,需以機器實際配置爲準),根據文檔,咱們能夠適當調大,至於上限是多少,文檔沒有給出說明,我也不清楚。我的認爲這種方法只能對TIME_WAIT過多的問題起到緩解做用,隨着訪問壓力的持續,該出現的問題早晚仍是會出現,治標不治本。
        2)開啓tcp_tw_recycle選項:在shell終端輸入命令」echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle」能夠開啓該配置。
        須要明確的是:其實TIME_WAIT狀態的socket是否被快速回收是由tcp_tw_recycle和tcp_timestamps兩個配置項共同決定的,只不過因爲tcp_timestamps默認就是開啓的,故大多數文章只提到設置tcp_tw_recycle爲1。更詳細的說明(分析kernel源碼)可參見這篇文章
        還須要特別注意的是:當client與server之間有如NAT這類網絡轉換設備時,開啓tcp_tw_recycle選項可能會致使server端drop(直接發送RST)來自client的SYN包。具體的案例及緣由分析,能夠參考這裏這裏這裏以及這裏的分析,本文再也不贅述。
        3)開啓tcp_tw_reuse選項:echo1 > /proc/sys/net/ipv4/tcp_tw_reuse。該選項也是與tcp_timestamps共同起做用的,另外socket reuse也是有條件的,具體說明請參見這篇文章。查了不少資料,與在用到NAT或FireWall的網絡環境下開啓tcp_tw_recycle後可能帶來反作用相比,貌似沒有發現tcp_tw_reuse引發的網絡問題。
        3.2 修改應用程序
       具體來講,能夠細分爲兩種方式:
        1)將TCP短鏈接改造爲長鏈接。一般狀況下,若是發起鏈接的目標也是本身可控制的服務器時,它們本身的TCP通訊最好採用長鏈接,避免大量TCP短鏈接每次創建/釋放產生的各類開銷;若是創建鏈接的目標是不受本身控制的機器時,可否使用長鏈接就須要考慮對方機器是否支持長鏈接方式了。
        2)經過getsockopt/setsockoptapi設置socket的SO_LINGER選項,關於SO_LINGER選項的設置方法,《UNP Volume1》一書7.5節給出了詳細說明,想深刻理解的同窗能夠去查閱該教材,也能夠參考這篇文章,講的還算清楚。

4. 須要補充說明的問題
        咱們說TIME_WAIT過多可能引發沒法對外創建新鏈接,其實有一個例外但比較常見的狀況:S模塊做爲WebServer部署在服務器上,綁定本地某個端口;客戶端與S間爲短鏈接,每次交互完成後由S主動斷開鏈接。這樣,當客戶端併發訪問次數很高時,S模塊所在的機器可能會有大量處於TIME_WAIT狀態的TCP鏈接。但因爲服務器模塊綁定了端口,故在這種狀況下,並不會引發「因爲TIME_WAIT過多致使沒法創建新鏈接」的問題。也就是說,本文討論的狀況,一般只會在每次由操做系統分配隨機端口的程序運行的機器上出現(每次分配隨機端口,致使後面無故口可用)。

 

 

延伸閱讀:kernel怎麼說

 

 

tcp_tw_reuse選項的含義以下(http://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt):
tcp_tw_reuse - BOOLEAN
Allow to reuse TIME-WAIT sockets for new connections when it is
safe from protocol viewpoint. Default value is 0.
    
這裏的關鍵在於「協議什麼狀況下認爲是安全的」,因爲環境限制,沒有辦法進行驗證,經過看源碼簡單分析了一下。
=====linux-2.6.37 net/ipv4/tcp_ipv4.c 114=====
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
struct tcp_sock *tp = tcp_sk(sk);


/* With PAWS, it is safe from the viewpoint
  of data integrity. Even without PAWS it is safe provided sequence
  spaces do not overlap i.e. at data rates <= 80Mbit/sec.


  Actually, the idea is close to VJ's one, only timestamp cache is
  held not per host, but per port pair and TW bucket is used as state
  holder.


  If TW bucket has been already destroyed we fall back to VJ's scheme
  and use initial timestamp retrieved from peer table.
*/
    //從代碼來看,tcp_tw_reuse選項和tcp_timestamps選項也必須同時打開;不然tcp_tw_reuse就不起做用
    //另外,所謂的「協議安全」,從代碼來看應該是收到最後一個包後超過1s
if (tcptw->tw_ts_recent_stamp &&
   (twp == NULL || (sysctl_tcp_tw_reuse &&
    get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2;
if (tp->write_seq == 0)
tp->write_seq = 1;
tp->rx_opt.ts_recent  = tcptw->tw_ts_recent;
tp->rx_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
sock_hold(sktw);
return 1;
}


return 0;

}

 

總結一下:
1)tcp_tw_reuse選項和tcp_timestamps選項也必須同時打開;
2)重用TIME_WAIT的條件是收到最後一個包後超過1s。


官方手冊有一段警告:
It should not be changed without advice/request of technical
experts.
對於大部分局域網或者公司內網應用來講,知足條件2)都是沒有問題的,所以官方手冊裏面的警告其實也沒那麼可怕:)

 

 

 

 

最後下面的連接中有一系列仔細分析這個問題的:

http://blog.csdn.net/yah99_wolf/article/category/539413

很是棒!

相關文章
相關標籤/搜索