結合內核源碼來看如何調整影響TIME_WAIT狀態套接字數量的參數


  這篇文件主要討論tcp_max_tw_buckets、tcp_timestamps、tcp_tw_recycle、tcp_tw_reuse和tcp_fin_timeout參數。

  測試的時候看到系統日志中不斷地出現“TCP: time wait bucket table overflow”的信息。在代碼中搜索了一下,看到這條日志是在tcp_time_wait()函數中輸出的,輸出這條日志是在局部變量tw為NULL的情況下。局部變量tw的默認值為NULL, 在下面的代碼中初始化:

 

    if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)
        tw = inet_twsk_alloc(sk, state);

其中tcp_death_row.tw_count是當前TIME_WAIT狀態套接字的數量,tcp_death_row.sysctl_max_tw_buckets的值是系統參數tcp_max_tw_buckets的值,如果前者小於后者,則tw的值為NULL。假設分配內存失敗,tw的值也為NULL。所以在TIME_WAIT套接字數量超過系統限制或者內存不足時,就會輸出“TCP: time wait bucket table overflow”的日志信息,如下所示:

    if (tw != NULL) {
        ......

    } else {
        /* Sorry, if we're out of memory, just CLOSE this
         * socket up.  We've got bigger problems than
         * non-graceful socket closings.
         */
        LIMIT_NETDEBUG(KERN_INFO "TCP: time wait bucket table overflow\n");
    }

  我這里的問題是因為tcp_max_tw_buckets的值設置的太小了,調整大了之后就OK了。如果系統當前TIME_WAIT狀態的套接字數量小於系統限制,出現這樣的問題就是嚴重的內存泄露了。后來google了一下看怎么減小TIME_WAIT套接字的數量,設置的選項大致就是摘要中提到的那幾個選項,不過大多數人都是幾乎全都給設置,也根本沒有考慮是否有用或者是否跟自己的業務相符合,看了內核代碼之后發現這些選項並不是什么情況下都能減小time-wait套接字的數量,有些甚至會適得其反,下面來看內核是如何處理的。

 

一、tcp_max_tw_buckets參數
  tcp_max_tw_buckets參數是系統中TIME_WAIT套接字的最大數量。假如tcp_max_tw_buckets的值設的太小,否則會導致部分連接沒法進入TIME_WAIT狀態,TCP連接可能會不正常關閉,數據包會重傳。假如tcp_max_tw_buckets的值設置的太大,TIME_WAIT狀態套接字占用的內容可能很大。這個值通常設置的大一些比較好,利用空間來給內核足夠的時間來清理之前的TIME_WAIT狀態的套接字,然后再結合其他參數來減小TIME_WAIT套接字的影響。這個參數對服務器端和客戶端都適用。

二、tcp_timestamps參數和tcp_tw_recycle參數
  tcp_timestamps參數用來設置是否啟用時間戳選項,tcp_tw_recycle參數用來啟用快速回收TIME_WAIT套接字。tcp_timestamps參數會影響到tcp_tw_recycle參數的效果。如果沒有時間戳選項的話,tcp_tw_recycle參數無效,代碼如下:

 

if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
        recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);

如果沒有時間戳選項,tp->rx_opt.ts_recent_stamp的值為0,這樣局部變量recycle_ok的值為0,在后面就會使用默認的時間TCP_TIMEWAIT_LEN(60s)作為TIME_WAIT狀態的時間長度,如下所示:

 

 

       if (recycle_ok) {
            tw->tw_timeout = rto;
        } else {
            tw->tw_timeout = TCP_TIMEWAIT_LEN;
            if (state == TCP_TIME_WAIT)
                timeo = TCP_TIMEWAIT_LEN;
        }

所以在設置tcp_tw_recycle參數后要檢查一下tcp_timestamps參數是否設置。
  接下來對tcp_tw_recycle參數的討論在tcp_timestamps設置的前提下進行。
  sock結構進入TIME_WAIT狀態有兩種情況:一種是在真正進入了TIME_WAIT狀態,還有一種是真實的狀態是FIN_WAIT_2的TIME_WAIT狀態。之所以讓FIN_WAIT_2狀態在沒有接收到FIN包的情況下也可以進入TIME_WAIT狀態是因為tcp_sock結構占用的資源要比tcp_timewait_sock結構占用的資源多,而且在TIME_WAIT下也可以處理連接的關閉。內核在處理時通過inet_timewait_sock結構的tw_substate成員來區分這種兩種情況。如果是第一種情況,在調用tcp_time_wait()時指定的超時時間timeo參數的值為0,如果沒有設置tcp_tw_recycle參數,TIME_WAIT狀態持續的時間是默認值TCP_TIMEWAIT_LEN(60s);如果設置tcp_tw_recycle參數,TIME_WAIT狀態持續的時間為局部變量rto的值。如果是第二種情況,在調用tcp_time_wait()時指定的超時時間timeo不為0,決定的是在子狀態下等待的時間,如果在FIN_WAIT_2狀態下接收到FIN包,在真正的TIME_WAIT狀態下等待的時間是由tw->tw_timeout成員決定的。同樣,在設置tcp_tw_recycle參數的情況下,tw->tw_timeout的值為rto,否則為TCP_TIMEWAIT_LEN。所以tcp_tw_recycle參數如果要實現對回收TIME_WAIT狀態套接字的加速,需要這個時間rto小於TCP_TIMEWAIT_LEN。rto的值由下面的式子計算:

const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);

其中icsk->icsk_rto的值是超時重傳的時間,這個值是根據網絡情況動態計算的。rto的值為icsk->icsk_rto的3.5倍。在網絡比較好的情況下,rto的值會小於TCP_TIMEWAIT_LEN,從而達到加速的目的;但是如果在網絡情況比較差,也就是說客戶端和服務器端往返的時間比較長的情況下,rto的值有可能會大於TCP_TIMEWAIT_LEN,這種情況下反而適得其反,這種情況通常是由客戶端引起的。所以在設置tcp_tw_recycle的時候要考慮到客戶端的情況。
  下面在來看看為什么rto的值要選擇為icsk->icsk_rto的3.5倍,也就是RTO*3.5,而不是2倍、4倍呢?我們知道,在FIN_WAIT_2狀態下接收到FIN包后,會給對端發送ACK包,完成TCP連接的關閉。但是最后的這個ACK包可能對端沒有收到,在過了RTO(超時重傳時間)時間后,對端會重新發送FIN包,這時需要再次給對端發送ACK包,所以TIME_WAIT狀態的持續時間要保證對端可以重傳兩次FIN包。如果重傳兩次的話,TIME_WAIT的時間應該為RTO*(0.5+0.5+0.5)=RTO*1.5,但是這里卻是RTO*3.5。這是因為在重傳情況下,重傳超時時間采用一種稱為“指數退避”的方式計算。例如:當重傳超時時間為1S的情況下發生了數據重傳,我們就用重傳超時時間為2S的定時器來重傳數據,下一次用4S,一直增加到64S為止(參見tcp_retransmit_timer())。所以這里的RTO*3.5=RTO*0.5+RTO*1+RTO*2,其中RTO*0.5是第一次發送ACK的時間到對端的超時時間(系數就是乘以RTO的值),RTO*1是對端第一次重傳FIN包到ACK包到達對端的超時時間,RTO*2是對端第二次重傳FIN包到ACK包到達對端的超時時間。注意,重傳超時時間的指數退避操作(就是乘以2)是在重傳之后執行的,所以第一次重傳的超時時間和第一次發送的超時時間相同。整個過程及時間分布如下圖所示(注意:箭頭雖然指向對端,只是用於描述過程,數據包並未被接收到):

 

 

三、tcp_tw_reuse參數
  tcp_tw_reuse參數用來設置是否可以在新的連接中重用TIME_WAIT狀態的套接字。注意,重用的是TIME_WAIT套接字占用的端口號,而不是TIME_WAIT套接字的內存等。這個參數對客戶端有意義,在主動發起連接的時候會在調用的inet_hash_connect()中會檢查是否可以重用TIME_WAIT狀態的套接字。如果你在服務器段設置這個參數的話,則沒有什么作用,因為服務器端ESTABLISHED狀態的套接字和監聽套接字的本地IP、端口號是相同的,沒有重用的概念。但並不是說服務器端就沒有TIME_WAIT狀態套接字。
四、tcp_fin_timeout參數
  有些人對這個參數會有誤解,認為這個參數是用來設置TIME_WAIT狀態持續的時間的。linux的內核文檔說的很明白,這個參數是用來設置保持在FIN_WAIT_2狀態的時間,原文如下():

 

 

tcp_fin_timeout - INTEGER Time to hold socket in state FIN-WAIT-2, if it was closed
    by our side. Peer can be broken and never close its side,
    or even died unexpectedly. Default value is 60sec.
    Usual value used in 2.2 was 180 seconds, you may restore
    it, but remember that if your machine is even underloaded WEB server, you risk to overflow memory with kilotons of dead sockets,
    FIN-WAIT-2 sockets are less dangerous than FIN-WAIT-1,
    because they eat maximum 1.5K of memory, but they tend
    to live longer.    Cf. tcp_max_orphans.

  如果是正常的處理流程就是在FIN_WAIT_2情況下接收到FIN進入到TIME_WAIT的情況,tcp_fin_timeout參數對處於TIME_WAIT狀態的時間沒有任何影響,但是如果這個參數設的比較小,會縮短從FIN_WAIT_1到TIME_WAIT的時間,從而使連接更早地進入TIME_WAIT狀態。狀態開始的早,等待相同的時間,結束的也早,客觀上也加速了TIME_WAIT狀態套接字的清理速度。
  如果在FIN_WAIT_2狀態下沒有接收到FIN而進入TIME_WAIT狀態(FIN_WAIT_2狀態超時或者超時時間大小、TCP_LINGER2等影響),在初始進入TIME_WAIT狀態時使用的超時時間為tcp_fin_time()計算出來的,tcp_fin_timeout參數的值可能會影響計算結果,取決於TCP_LINGER2選項。但是,要知道,在TIME_WAIT狀態下也有兩個子狀態(inet_timewait_sock的tw_substate成員區分):TIME_WAIT和FIN_WAIT_2。在子狀態FIN_WAIT_2狀態下接收到FIN包進入子狀態TIME_WAIT時,會重置定時器,此時的超時時間要么是3.5*RTO,要么是默認值TCP_TIMEWAIT_LEN(60s),所以嚴格意義上講,tcp_fin_timeout參數只是用來設置保持在FIN_WAIT_2狀態的時間。
  在FIN_WAIT_2狀態下沒有接收到FIN包就進入TIME_WAIT的情況下,如果tcp_fin_timeout的值設置的太小,可能會導致TIME_WAIT套接字(子狀態為FIN_WAIT_2)過早地被釋放,這樣對端發送的FIN(短暫地延遲或者本來就是正常的時間到達)到達時就沒有辦法處理,導致連接不正常關閉,所以tcp_fin_timeout參數的值並不是越小越好,通常設置為30S比較合適。
  通過上面的討論可以看出,在設置內核參數時一定要慎重,結合自己的業務情況,多看一看文檔,弄清楚這些參數都會造成哪些影響,不要盲目地修改。
  文章中如果有錯誤,請幫忙指正,以免誤導他人,多謝!

 

 


免責聲明!

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



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