1. 程式人生 > >TIME_WAIT過多的解決辦法

TIME_WAIT過多的解決辦法

執行主動關閉的那端經歷了這個狀態,並停留MSL(最長分節生命期)的2倍,即2MSL。

TIME_WAIT存在的兩個理由:

1 可靠的實現TCP全雙工連線的終止

2 允許老的重複的分節在網路上的消逝

第一個:如果客戶端不維持TIME_WAIT狀態,那麼將響應給服務端一個RST,該分節被伺服器解釋成一個錯誤。如果TCP打算執行所有必要的工作以徹底終止某個連線上兩個方向的資料流,那麼必須正確的處理。執行主動關閉的那一端是處於TIME_WAIT狀態的那一端。

第二個:端到端的連線關閉後,過一段時間相同的ip和埠間進行連線,後一個連線成為前一個連線的化身。TCP必須防止來自某個連線的老的重複分組在該連線已經終止後再現,從而被誤解成處於同一個連線的某個新的化身。為做到這一點,TCP將不給處於TIME_WAIT狀態的連線發起新的化身

TIME_WAIT狀態的持續時間是MSL的2倍,使得某個方向上的分組最多存活MSL秒被丟棄,另一個方向上的應答最多存活MSL秒被丟棄,這樣保證每建立一個TCP連線的時候,來自連線先前的化身的老的重複分組都已在網路中消逝。

那麼TIME_WAIT狀態有什麼危害麼?

首先要明白兩個概念長連線和短連線,

短連線:

我們模擬一下TCP短連線的情況,client向server發起連線請求,server接到請求,然後雙方建立連線。client向server傳送訊息,server迴應client,然後一次讀寫就完成了,這時候雙方任何一個都可以發起close操作,不過一般都是client先發起close操作。為什麼呢,一般的server不會回覆完client後立即關閉連線的,當然不排除有特殊的情況。從上面的描述看,短連線一般只會在client/server間傳遞一次讀寫操作

短連線最大的優點是方便,特別是指令碼語言,由於執行完畢後腳本語言的程序就結束了,基本上都是用短連線。
但短連線最大的缺點是將佔用大量的系統資源,例如:本地埠、socket控制代碼。
導致這個問題的原因其實很簡單:tcp協議層並沒有長短連線的概念,因此不管長連線還是短連線,連線建立->資料傳輸->連線關閉的流程和處理都是一樣的。

長連線:

接下來我們再模擬一下長連線的情況,client向server發起連線,server接受client連線,雙方建立連線。Client與server完成一次讀寫之後,它們之間的連線並不會主動關閉,後續的讀寫操作會繼續使用這個連線。

首先說一下TCP/IP詳解上講到的TCP保活功能,保活功能主要為伺服器應用提供,伺服器應用希望知道客戶主機是否崩潰,從而可以代表客戶使用資源。如果客戶已經消失,使得伺服器上保留一個半開放的連線,而伺服器又在等待來自客戶端的資料,則伺服器將應遠等待客戶端的資料,保活功能就是試圖在伺服器端檢測到這種半開放的連線。

如果一個給定的連線在兩小時內沒有任何的動作,則伺服器就向客戶發一個探測報文段,客戶主機必須處於以下4個狀態之一:

客戶主機依然正常執行,並從伺服器可達。客戶的TCP響應正常,而伺服器也知道對方是正常的,伺服器在兩小時後將保活定時器復位。
客戶主機已經崩潰,並且關閉或者正在重新啟動。在任何一種情況下,客戶的TCP都沒有響應。服務端將不能收到對探測的響應,並在75秒後超時。伺服器總共傳送10個這樣的探測 ,每個間隔75秒。如果伺服器沒有收到一個響應,它就認為客戶主機已經關閉並終止連線。
客戶主機崩潰並已經重新啟動。伺服器將收到一個對其保活探測的響應,這個響應是一個復位,使得伺服器終止這個連線。
客戶機正常執行,但是伺服器不可達,這種情況與2類似,TCP能發現的就是沒有收到探查的響應。
從上面可以看出,TCP保活功能主要為探測長連線的存活狀況,不過這裡存在一個問題,存活功能的探測週期太長,還有就是它只是探測TCP連線的存活,屬於比較斯文的做法,遇到惡意的連線時,保活功能就不夠使了。

在長連線的應用場景下,client端一般不會主動關閉它們之間的連線,Client與server之間的連線如果一直不關閉的話,會存在一個問題,隨著客戶端連線越來越多,server早晚有扛不住的時候,這時候server端需要採取一些策略,如關閉一些長時間沒有讀寫事件發生的連線,這樣可以避免一些惡意連線導致server端服務受損;如果條件再允許就可以以客戶端機器為顆粒度,限制每個客戶端的最大長連線數,這樣可以完全避免某個蛋疼的客戶端連累後端服務。

長連線和短連線的產生在於client和server採取的關閉策略,具體的應用場景採用具體的策略,沒有十全十美的選擇,只有合適的選擇。

下面主要討論TIME_WAIT對短連線的影響:

正常的TCP客戶端連線在關閉後,會進入一個TIME_WAIT的狀態,持續的時間一般在1~4分鐘,對於連線數不高的場景,1~4分鐘其實並不長,對系統也不會有什麼影響,
但如果短時間內(例如1s內)進行大量的短連線,則可能出現這樣一種情況:客戶端所在的作業系統的socket埠和控制代碼被用盡,系統無法再發起新的連線!


舉例來說:假設每秒建立了1000個短連線(Web場景下是很常見的,例如每個請求都去訪問memcached),假設TIME_WAIT的時間是1分鐘,則1分鐘內需要建立6W個短連線,
由於TIME_WAIT時間是1分鐘,這些短連線1分鐘內都處於TIME_WAIT狀態,都不會釋放,而Linux預設的本地埠範圍配置是:net.ipv4.ip_local_port_range = 32768    61000
不到3W,因此這種情況下新的請求由於沒有本地埠就不能建立了。

可以通過如下方式來解決這個問題:

1)可以改為長連線,但代價較大,長連線太多會導致伺服器效能問題,而且PHP等指令碼語言,需要通過proxy之類的軟體才能實現長連線;
2)修改ipv4.ip_local_port_range,增大可用埠範圍,但只能緩解問題,不能根本解決問題;
3)客戶端程式中設定socket的SO_LINGER選項
4)客戶端機器開啟tcp_tw_recycle和tcp_timestamps選項
5)客戶端機器開啟tcp_tw_reuse和tcp_timestamps選項;
6)客戶端機器設定tcp_max_tw_buckets為一個很小的值

方法2:

#檢視系統本地可用埠極限值
cat /proc/sys/net/ipv4/ip_local_port_range

用這條命令會返回兩個數字,預設是:32768 61000,說明這臺機器本地能向外連線61000-32768=28232個連線,注意是本地向外連線,不是這臺機器的所有連線,不會影響這臺機器的80埠的對外連線數。但這個數字會影響到代理伺服器(nginx)對app伺服器的最大連線數,因為nginx對app是用的非同步傳輸,所以這個環節的連線速度很快,所以堆積的連線就很少。假如nginx對app伺服器之間的頻寬出了問題或是app伺服器有問題,那麼可能使連線堆積起來,這時可以通過設定nginx的代理超時時間,來使連線儘快釋放掉,一般來說極少能用到28232個連線。
因為有軟體使用了40000埠監聽,常常出錯的話,可以通過設定ip_local_port_range的最小值來解決:
echo "40001 61000" > /proc/sys/net/ipv4/ip_local_port_range

但是這麼做很顯然把系統可用埠數減少了,這時可以把ip_local_port_range的最大值往上調,但是好習慣是使用不超過32768的埠來偵聽服務,另外也不必要去修改ip_local_port_range數值成1024 65535之類的,意義不大。

方法3:

SO_LINGER是一個socket選項,通過setsockopt API進行設定,使用起來比較簡單,但其實現機制比較複雜,且字面意思上比較難理解。
解釋最清楚的當屬《Unix網路程式設計卷1》中的說明(7.5章節),這裡簡單摘錄:
SO_LINGER的值用如下資料結構表示:
struct linger {
     int l_onoff; /* 0 = off, nozero = on */
     int l_linger; /* linger time */
};


其取值和處理如下:
1、設定 l_onoff為0,則該選項關閉,l_linger的值被忽略,等於核心預設情況,close呼叫會立即返回給呼叫者,如果可能將會傳輸任何未傳送的資料;
2、設定 l_onoff為非0,l_linger為0,則套介面關閉時TCP夭折連線,TCP將丟棄保留在套介面傳送緩衝區中的任何資料併發送一個RST給對方,
   而不是通常的四分組終止序列,這避免了TIME_WAIT狀態;
3、設定 l_onoff 為非0,l_linger為非0,當套介面關閉時核心將拖延一段時間(由l_linger決定)。
   如果套介面緩衝區中仍殘留資料,程序將處於睡眠狀態,直 到(a)所有資料傳送完且被對方確認,之後進行正常的終止序列(描述字訪問計數為0)
   或(b)延遲時間到。此種情況下,應用程式檢查close的返回值是非常重要的,如果在資料傳送完並被確認前時間到,close將返回EWOULDBLOCK錯誤且套介面傳送緩衝區中的任何資料都丟失。
   close的成功返回僅告訴我們傳送的資料(和FIN)已由對方TCP確認,它並不能告訴我們對方應用程序是否已讀了資料。如果套介面設為非阻塞的,它將不等待close完成。
   
第一種情況其實和不設定沒有區別,第二種情況可以用於避免TIME_WAIT狀態,但在Linux上測試的時候,並未發現傳送了RST選項,而是正常進行了四步關閉流程,
初步推斷是“只有在丟棄資料的時候才傳送RST”,如果沒有丟棄資料,則走正常的關閉流程。
檢視Linux原始碼,確實有這麼一段註釋和原始碼:
=====linux-2.6.37 net/ipv4/tcp.c 1915=====
/* As outlined in RFC 2525, section 2.17, we send a RST here because
* data was lost. To witness the awful effects of the old behavior of
* always doing a FIN, run an older 2.1.x kernel or 2.0.x, start a bulk
* GET in an FTP client, suspend the process, wait for the client to
* advertise a zero window, then kill -9 the FTP client, wheee...
* Note: timeout is always zero in such a case.
*/
if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, sk->sk_allocation);

另外,從原理上來說,這個選項有一定的危險性,可能導致丟資料,使用的時候要小心一些,但我們在實測libmemcached的過程中,沒有發現此類現象,
應該是和libmemcached的通訊協議設定有關,也可能是我們的壓力不夠大,不會出現這種情況。

第三種情況其實就是第一種和第二種的折中處理,且當socket為非阻塞的場景下是沒有作用的。
對於應對短連線導致的大量TIME_WAIT連線問題,個人認為第二種處理是最優的選擇,libmemcached就是採用這種方式,
從實測情況來看,開啟這個選項後,TIME_WAIT連線數為0,且不受網路組網(例如是否虛擬機器等)的影響。

方法4:

tcp_tw_recycle和tcp_timestamps】
參考官方文件(http://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt),tcp_tw_recycle解釋如下:
tcp_tw_recycle選項作用為:Enable fast recycling TIME-WAIT sockets. Default value is 0.
tcp_timestamps選項作用為:Enable timestamps as defined in RFC1323. Default value is 1.

這兩個選項是linux核心提供的控制選項,和具體的應用程式沒有關係,而且網上也能夠查詢到大量的相關資料,但資訊都不夠完整,最主要的幾個問題如下;
1)快速回收到底有多快?
2)有的資料說只要開啟tcp_tw_recycle即可,有的又說要tcp_timestamps同時開啟,具體是哪個正確?
3)為什麼從虛擬機器NAT出去發起客戶端連線時選項無效,非虛擬機器連線就有效?

為了回答上面的疑問,只能看程式碼,看出一些相關的程式碼供大家參考:
=====linux-2.6.37 net/ipv4/tcp_minisocks.c 269======
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
struct inet_timewait_sock *tw = NULL;
const struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcp_sock *tp = tcp_sk(sk);
int recycle_ok = 0;

    //判斷是否快速回收,這裡可以看出tcp_tw_recycle和tcp_timestamps兩個選項都開啟的時候才進行快速回收,
    //且還有進一步的判斷條件,後面會分析,這個進一步的判斷條件和第三個問題有關
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);

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

if (tw != NULL) {
struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
        //計算快速回收的時間,等於 RTO * 3.5,回答第一個問題的關鍵是RTO(Retransmission Timeout)大概是多少
const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);
        
        //。。。。。。此處省略很多程式碼。。。。。。
        
    if (recycle_ok) {
            //設定快速回收的時間
tw->tw_timeout = rto;
} else {
tw->tw_timeout = TCP_TIMEWAIT_LEN;
if (state == TCP_TIME_WAIT)
timeo = TCP_TIMEWAIT_LEN;
}
        
        //。。。。。。此處省略很多程式碼。。。。。。
}

RFC中有關於RTO計算的詳細規定,一共有三個:RFC-793、RFC-2988、RFC-6298,Linux的實現是參考RFC-2988。
對於這些演算法的規定和Linuxde 實現,有興趣的同學可以自己深入研究,實際應用中我們只要記住Linux如下兩個邊界值:
=====linux-2.6.37 net/ipv4/tcp.c 126================
#define TCP_RTO_MAX ((unsigned)(120*HZ))
#define TCP_RTO_MIN ((unsigned)(HZ/5))
==========================================
這裡的HZ是1s,因此可以得出RTO最大是120s,最小是200ms,對於區域網的機器來說,正常情況下RTO基本上就是200ms,因此3.5 RTO就是700ms
也就是說,快速回收是TIME_WAIT的狀態持續700ms,而不是正常的2MSL(Linux是1分鐘,請參考:include/net/tcp.h 109行TCP_TIMEWAIT_LEN定義)。
實測結果也驗證了這個推論,不停的檢視TIME_WAIT狀態的連線,偶爾能看到1個。

最後一個問題是為什麼從虛擬機發起的連線即使設定了tcp_tw_recycle和tcp_timestamps,也不會快速回收,繼續看程式碼:
tcp_time_wait函式中的程式碼行:recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);對應的實現如下:
=====linux-2.6.37 net/ipv4/tcp_ipv4.c 1772=====
int tcp_v4_remember_stamp(struct sock *sk)
{
    //。。。。。。此處省略很多程式碼。。。。。。

    //當獲取對端資訊時,進行快速回收,否則不進行快速回收
if (peer) {
if ((s32)(peer->tcp_ts - tp->rx_opt.ts_recent) <= 0 ||
   ((u32)get_seconds() - peer->tcp_ts_stamp > TCP_PAWS_MSL &&
    peer->tcp_ts_stamp <= (u32)tp->rx_opt.ts_recent_stamp)) {
peer->tcp_ts_stamp = (u32)tp->rx_opt.ts_recent_stamp;
peer->tcp_ts = tp->rx_opt.ts_recent;
}
if (release_it)
inet_putpeer(peer);
return 1;
}

return 0;
}
上面這段程式碼應該就是測試的時候虛擬機器環境不會釋放的原因,當使用虛擬機器NAT出去的時候,伺服器無法獲取隱藏在NAT後的機器資訊。
生產環境也出現了設定了選項,但TIME_WAIT連線數達到4W多的現象,可能和虛擬機器有關,也可能和組網有關。

總結一下:
1)快速回收到底有多快?
區域網環境下,700ms就回收;
2)有的資料說只要開啟tcp_tw_recycle即可,有的又說要tcp_timestamps同時開啟,具體是哪個正確?
需要同時開啟,但預設情況下tcp_timestamps就是開啟的,所以會有人說只要開啟tcp_tw_recycle即可;
3)為什麼從虛擬機發起客戶端連線時選項無效,非虛擬機器連線就有效?
和網路組網有關係,無法獲取對端資訊時就不進行快速回收;

綜合上面的分析和總結,可以看出這種方法不是很保險,在實際應用中可能受到虛擬機器、網路組網、防火牆之類的影響從而導致不能進行快速回收。

附:
1)tcp_timestamps的說明詳見RF1323,和TCP的擁塞控制(Congestion control)有關。
2)開啟此選項,可能導致無法連線,請參考:http://www.pagefault.info/?p=416 

方法5:

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)都是沒有問題的,因此官方手冊裡面的警告其實也沒那麼可怕:)

方法6:

參考官方文件(http://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt),解釋如下:
tcp_max_tw_buckets - INTEGER
Maximal number of timewait sockets held by system simultaneously.
If this number is exceeded time-wait socket is immediately destroyed
and warning is printed. 
官方文件沒有說明預設值,通過幾個系統的簡單驗證,初步確定預設值是180000。

通過原始碼檢視發現,這個選項比較簡單,其實現程式碼如下:
=====linux-2.6.37 net/ipv4/tcp_minisocks.c 269======
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
struct inet_timewait_sock *tw = NULL;
const struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcp_sock *tp = tcp_sk(sk);
int recycle_ok = 0;

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

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

if (tw != NULL) {
        //分配成功,進行TIME_WAIT狀態處理,此處略去很多程式碼
    else {
        //分配失敗,不進行處理,只記錄日誌: TCP: time wait bucket table overflow
/* Sorry, if we're out of memory, just CLOSE this
* socket up.  We've got bigger problems than
* non-graceful socket closings.
*/
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPTIMEWAITOVERFLOW);
}

tcp_update_metrics(sk);
tcp_done(sk);
}
實測結果驗證,配置為100,TIME_WAIT連線數就穩定在100,且不受組網和其它配置的影響。

官方手冊中有一段警告:
    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.
基本意思是這個用於防止Dos攻擊,我們不應該人工減少,如果網路條件需要的話,反而應該增加。
但其實對於我們的區域網或者公司內網應用來說,這個風險並不大。

參考:

http://blog.csdn.net/yunhua_lee