網絡優化之net.ipv4.tcp_tw_recycle參數


不要在linux上啟用net.ipv4.tcp_tw_recycle參數

本文為翻譯英文BLOG《Coping with the TCP TIME-WAIT state on busy Linux servers》,但並非完整的翻譯,譯者CFC4N對原文理解后,進行了調整,增加了相關論點論據,跟原文稍有不同。翻譯的目的,是為了加深自己知識點的記憶,以及分享給其他朋友,或許對他們也有幫助。文章比較長,沒耐心請點關閉。

不要啟用 net.ipv4.tcp_tw_recycle

linux 內核文檔中,對net.ipv4.tcp_tw_recycle的描述並不是很明確。

tcp_tw_recycle (Boolean; default: disabled; since Linux 2.4)[譯者注:來自linux man tcp的描述]
Enable fast recycling of TIME-WAIT sockets. Enabling this option is not recommended since this causes
problems when working with NAT (Network Address Translation).
啟用TIME-WAIT狀態sockets的快速回收,這個選項不推薦啟用。在NAT(Network Address Translation)網絡下,會導致大量的TCP連接建立錯誤。

與其功能相似的參數net.ipv4.tcp_tw_reuse,手冊里稍微有點描述,如下:

tcp_tw_reuse (Boolean; default: disabled; since Linux 2.4.19/2.6)
Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. It
should not be changed without advice/request of technical experts.
//從協議設計上來看,對於TIME-WAIT狀態的sockets重用到新的TCP連接上來說,是安全的。(用於客戶端時的配置)

這里的注釋說明非常的少,我們發現,網上很多linux參數調整指南都建議把這些參數net.ipv4.tcp_tw_recycle 設置1「啟用」,用於快速減少在TIME-WAIT狀態TCP連接數。
但是,在TCP(7)手冊中,參數net.ipv4.tcp_tw_recycle 非常蛋疼,尤其是在普通用戶家中,有多台設備,或者網吧、公司等多台設備,共用同一個NAT設備環境下,TW回收選項是很有問題的面向公共服務器作為它不會把手連接兩台不同的計算機上,這問題很難發現,無從下手。

Enable fast recycling of TIME-WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation).
啟用TIME-WAIT狀態sockets的快速回收,這個選項不推薦啟用。在NAT(Network Address Translation)網絡下,會導致大量的TCP連接建立錯誤。如果沒有技術大神的指點的話,千萬不要去改動他。

下文將給予更詳細的解釋,希望可以糾正互聯網上錯誤的觀點,尤其是轉載比較多的內容,搜索時,往往排在前面,使用者往往接觸到的都是不嚴謹的或者是錯誤的知識點。

正如此文,在 net.ipv4.tcp_tw_recycle 控制參數中,盡管很多地方寫的是ipv4,但對ipv6同樣實用。此外,我們這里聊的是Linux TCP協議棧,在linux上可能會受到Netfilter影響,稍微有差異。

關於TCP連接的TIME-WAIT狀態,它是為何而生,存在的意義是什么?

讓我們回憶一下,什么是TCP TIME-WAIT狀態?如下圖
tcp-state-diagram

這圖中的流程不是很好理解,再看一張流程更清晰的圖

TCP狀態流程圖

TCP狀態流程圖

 

當TCP連接關閉之前,首先發起關閉的一方會進入TIME-WAIT狀態,另一方可以快速回收連接。
可以用ss -tan來查看TCP 連接的當前狀態

1
2
3
4
5
6
7
8
9
[cfc4n@localhost ~] #$ ss -tan
State      Recv-Q Send-Q                            Local Address:Port                              Peer Address:Port
LISTEN     0      128                                   127.0.0.1:9000                                         *:*
TIME-WAIT  0      0                                     127.0.0.1:9000                                 127.0.0.1:60274
TIME-WAIT  0      0                                     127.0.0.1:9000                                 127.0.0.1:60273
CLOSE-WAIT 431    0                                 115.29.188.27:60002                            110.75.102.62:80
ESTAB      0      208                               115.29.188.27:22                              180.167.20.210:2455
CLOSE-WAIT 221    0                                 115.29.188.27:42489                            42.156.166.25:80
FIN-WAIT-2 0      0                                 115.29.188.27:80                             222.246.178.104:27335

TIME-WAIT狀態的作用

對於TIME-WAIT狀態來說,有兩個作用
一、人盡皆知的是,防止上一個TCP連接的延遲的數據包(發起關閉,但關閉沒完成),被接收后,影響到新的TCP連接。(唯一連接確認方式為四元組:源IP地址、目的IP地址、源端口、目的端口),包的序列號也有一定作用,會減少問題發生的幾率,但無法完全避免。尤其是較大接收windows size的快速(回收)連接。RFC1137解釋了當TIME-WAIT狀態不足時將會發生什么。如果TIME-WAIT狀態連接沒有被快速回收,會避免什么問題呢?請看下面的例子:
duplicate-segment
縮短TIME-WAIT的時間后,延遲的TCP 包會被新建立的TCP連接接收。

二、另外一個作用是,當最后一個ACK丟失時,遠程連接進入LAST-ACK狀態,它可以確保遠程已經關閉當前TCP連接。如果沒有TIME-WAIT狀態,當遠程仍認為這個連接是有效的,則會繼續與其通訊,導致這個連接會被重新打開。當遠程收到一個SYN 時,會回復一個RST包,因為這SEQ不對,那么新的連接將無法建立成功,報錯終止。
last-ack
如果遠程因為最后一個ACK包丟失,導致停留在LAST-ACK狀態,將影響新建立具有相同四元組的TCP連接。

RFC 793中強調TIME-WAIT狀態必須是兩倍的MSL時間(max segment lifetime),在linux上,這個限制時間無法調整,寫死為1分鍾了,定義在include/net/tcp.h

1
2
3
4
5
6
7
8
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                   * state, about 60 seconds */
#define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LEN
                                  /* BSD style FIN_WAIT2 deadlock breaker.
                   * It used to be 3min, new value is 60sec,
                   * to combine FIN-WAIT-2 timeout with
                   * TIME-WAIT timer.
                   */

曾有人提議將TCP TIME-WAIT時間改為一個可以自定義配置的參數,但被拒絕了,其實,這對TCP規范,對TIME-WAIT來說,是利大於弊的。

那么問題來了

我們來看下,為什么這個狀態能影響到一個處理大量連接的服務器,從下面三個方面來說:

  • 新老連接(相同四元組)在TCP連接表中的slot復用避免
  • 內核中,socket結構體的內存占用
  • 額外的CPU開銷

ss -tan state time-wait|wc -l的結果,並不能說明這些問題。

Connection table slot連接表槽

處於TIME-WAIT狀態的TCP連接,在鏈接表槽中存活1分鍾,意味着另一個相同四元組(源地址,源端口,目標地址,目標端口)的連接不能出現,也就是說新的TCP(相同四元組)連接無法建立。

對於web服務器來說,目標地址、目標端口都是固定值。如果web服務器是在L7層的負載均衡后面,那么源地址更是固定值。在LINUX上,作為客戶端時,客戶端端口默認可分配的數量是3W個(可以在參數net.ipv4.up_local_port_range上調整)。
這意味着,在web服務器跟負載均衡服務器之間,每分鍾只有3W個端口是處於established狀態,也就大約500連接每秒。

如果TIME-WAIT狀態的socket出現在客戶端,那這個問題很容易被發現。調用connect()函數會返回EADDRNOTAVAIL,程序也會記錄相關的錯誤到日志。
如果TIME-WATI狀態的socket出現在服務端,問題會非常復雜,因為這里並沒有日志記錄,也沒有計數器參考。不過,可以列出服務器上當前所有四元組連接的數量來確認

1
2
3
4
5
6
7
8
9
[root@localhost ~] #$ ss -tan 'sport = :80' | awk '{print $(NF)" "$(NF-1)}' | sed 's/:[^ ]*//g' | sort | uniq -c
     696 10.24.2.30 10.33.1.64
    1881 10.24.2.30 10.33.1.65
    5314 10.24.2.30 10.33.1.66
    5293 10.24.2.30 10.33.1.67
    3387 10.24.2.30 10.33.1.68
    2663 10.24.2.30 10.33.1.69
    1129 10.24.2.30 10.33.1.70
   10536 10.24.2.30 10.33.1.73

解決辦法是,增加四元組的范圍,這有很多方法去實現。(以下建議的順序,實施難度從小到大排列)

  • 修改net.ipv4.ip_local_port_range參數,增加客戶端端口可用范圍。
  • 增加服務端端口,多監聽一些端口,比如81、82、83這些,web服務器前有負載均衡,對用戶友好。
  • 增加客戶端IP,尤其是作為負載均衡服務器時,使用更多IP去跟后端的web服務器通訊。
  • 增加服務端IP。

當然了,最后的辦法是調整net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle。但不到萬不得已,千萬別這么做,稍后再講。

內存

保持大量的連接時,當多為每一連接多保留1分鍾,就會多消耗一些服務器的內存。舉個栗子,如果服務器每秒處理了1W個新的TCP連接,那么服務器將會存貨1W/s*60s = 60W個TIME-WAIT狀態的TCP連接,那這將會占用多大的內存么?別擔心,少年,沒那么多。

首先,從應用的角度來看,一個TIME-WAIT狀態的socket不會消耗任何內存:socket已經關了。在內核中,TIME-WAIT狀態的socket,對於三種不同的作用,有三個不同的結構。
一、“TCP established hash table”的連接存儲哈希表(包括其他非established狀態的連接),當有新的數據包發來時,是用來定位查找存活狀態的連接的。
該哈希表的bucket都包括在TIME-WAIT連接列表以及正在活躍的連接列表中(netstat -antp命令的結果中,沒PID的TIME_WAIT狀態連接,跟有PID的活躍連接兩種)。
該哈希表的大小,取決於操作系統內存大小。在系統引導時,會打印出來,dmesg日志中可以看到。

1
2
dmesg | grep "TCP established hash table"
[    0.169348] TCP established hash table entries: 65536 (order: 8, 1048576 bytes)

這個數值,有可能被kernel啟動參數thash_entries(設置TCP連接哈希表的最大數目)的改動而將其覆蓋。

在TIME-WAIT狀態連接列表中,每一個元素都是一個tcp_timewait_sock結構體,其他狀態的連接都是tcp_sock結構體。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct tcp_timewait_sock {
     struct inet_timewait_sock tw_sk;
     u32    tw_rcv_nxt;
     u32    tw_snd_nxt;
     u32    tw_rcv_wnd;
     u32    tw_ts_offset;
     u32    tw_ts_recent;
     long   tw_ts_recent_stamp;
};
 
struct inet_timewait_sock {
     struct sock_common  __tw_common;
 
     int                     tw_timeout;
     volatile unsigned char  tw_substate;
     unsigned char           tw_rcv_wscale;
     __be16 tw_sport;
     unsigned int tw_ipv6only     : 1,
                  tw_transparent  : 1,
                  tw_pad          : 6,
                  tw_tos          : 8,
                  tw_ipv6_offset  : 16;
     unsigned long            tw_ttd;
     struct inet_bind_bucket *tw_tb;
     struct hlist_node        tw_death_node; #這個結構體叫做“death row”的connection hash
};

二、有一組叫做“death row”的連接列表,是用來終止TIME-WAIT狀態的連接的,這會在他們過期之前,開始申請。它占用的內存空間,跟在連接哈希表中的一樣。這個結構體hlist_node tw_death_node是inet_timewait_sock的一個成員,如上代碼的倒數第二行。

三、有個綁定端口的哈希表,存儲綁定端口跟其他參數,用來確保當前端口沒有被使用的,比如在listen監聽時的指定端口,或者連接其他socket時,系統動態分配的端口。該哈希表的大小跟連接哈希表大小一樣。

1
2
$ dmesg | grep "TCP bind hash table"
[    0.169962] TCP bind hash table entries: 65536 (order: 8, 1048576 bytes)

每個元素都是inet_bind_socket結構體。每個綁定的端口都會有一個元素。對於web服務器來說,它綁定的是80端口,其TIME-WAIT連接都是共享同一個entry的。另外,連接到遠程服務器的本地連接,他們的端口都是隨機分配的,並不共享其entry。
所以,我們只關心結構體tcp_timewait_sock跟結構體inet_bind_socket所占用的空間大小。每一個連到遠程,或遠程連到本地的一個TIME-WAIT狀態的連接,都有一個tcp_timewait_sock結構體。還有個結構體inet_bind_socket,只會在連到遠程的連接會存在,遠程連過來的連接沒這個結構體。

tcp_timewait_sock結構體的大小只有168 bytes,inet_bind_socket結構體為48bytes:

1
2
3
4
5
6
7
8
9
$ sudo apt-get install linux-image-$( uname -r)-dbg
[...]
$ gdb /usr/lib/debug/boot/vmlinux- $( uname -r)
( gdb ) print sizeof(struct tcp_timewait_sock)
  $1 = 168
( gdb ) print sizeof(struct tcp_sock)
  $2 = 1776
( gdb ) print sizeof(struct inet_bind_bucket)
  $3 = 48

所以,當服務器上有4W個連進來的連接進入TIME-WAIT狀態時,才用了10MB不到的內存。如果服務器上有4W個連接到遠程的連接進入TIME-WAIT狀態時,才用了2.5MB的內存。再來看下slabtop的結果,這里測試數據是5W個TIME-WAIT狀態的連接結果,其中4.5W是連接到遠程的連接:

1
2
3
4
$ sudo slabtop -o | grep -E '(^  OBJS|tw_sock_TCP|tcp_bind_bucket)'
   OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ /SLAB CACHE SIZE NAME                  
  50955  49725  97%    0.25K   3397       15     13588K tw_sock_TCP           
  44840  36556  81%    0.06K    760       59      3040K tcp_bind_bucket

命令執行結果原樣輸出,一個字符都沒動。TIME-WAIT狀態的連接占用內存非常的小。如果你的服務器上要處理每秒成千上萬的新建TCP連接,你可能需要多一點的內存才能 正確無誤的跟客戶端做數據通信。但TIME-WAIT狀態連接的內存占用,簡直可以無視。

CPU

在CPU這邊,查找一個空閑端口的操作,還是蠻珍貴的。這由inet_csk_get_port() 函數,加鎖,遍歷整個空閑端口列表實現。這個哈希表里條目數量大通常不是問題,如果服務器上存在大量連接到遠程TIME-WAIT狀態的連接(比如FPM連redis、memcache之類),都會同享相同的profile,這個特性會非常快的按照順序找到一個新的空閑端口。

其他解決辦法

如果你讀了上面的章節后,仍對TIME-WAIT狀態的連接存有疑問,那么接着看吧:

  • 禁用socket延遲關閉「譯者注1:以ubuntu 12.04為例,linger結構體定義在:/usr/src/linux-headers-3.2.0-23/include/linux/socket.h」
  • 禁用net.ipv4.tcp_tw_reuse
  • 禁用net.ipv4.tcp_tw_recycle
1
2
3
4
struct linger {
         int             l_onoff;        /* Linger active                */
         int             l_linger;       /* How long to linger for       */
};

當close被調用時,SOCKET需要延遲關閉(lingering),在內核buffers中的殘留數據將會發送到遠程地址,同時,socket會切換到TIME-WAIT狀態。如果禁用此選項,則調用close之后,底層也會關閉,不會將Buffers中殘留數據未發送的數據繼續發送。

不過呢,應用程序可以選擇禁用socket lingering延遲關閉行為。關於socket lingering 延遲關閉,下面兩個行為簡單描述一下:
第一種情況,close函數后,並不會直接終止該四元組連接序號,而是在buffers任何殘留數據都會被丟棄。該TCP連接將會收到一個RST的關閉信號,之后,服務端將立刻銷毀該(四元組)連接。 在這種做法中,不會再有TIME-WAIT狀態的SOCKET出現。第二種情況,如果當調用close函數后,socket發送buffer中仍然有殘留數據,此進程將會休眠,直到所有數據都發送完成並確認,或者所配置的linger計時器過期了。非阻塞socket可以設置不休眠。如上,這些過程都都在底層發生,這個機制確保殘留數據在配置的超時時間內都發送出去。 如果數據正常發送出去,close包也正常發送,那么將會轉換為TIME-WAIT狀態。其他異常情況下,客戶端將會收到RST的連接關閉信號,同時,服務端殘留數據會被丟棄。

這里的兩種情況,禁用socket linger延遲關閉不是萬金油。但在HAproxy,Nginx(反代)場景中,在TCP協議上層的應用上(比如HTTP),比較合適。同樣,也有很多無可厚非的理由不能禁用它。

net.ipv4.tcp_tw_reuse

TIME-WAIT狀態是為了防止不相關的延遲請求包被接受。但在某些特定條件下,很有可能出現,新建立的TCP連接請求包,被老連接(同樣的四元組,暫時還是TIME-WAIT狀態,回收中)的連接誤處理。RFC 1323 實現了TCP拓展規范,以保證網絡繁忙狀態下的高可用。除此之外,另外,它定義了一個新的TCP選項–兩個四字節的timestamp fields時間戳字段,第一個是TCP發送方的當前時鍾時間戳,而第二個是從遠程主機接收到的最新時間戳。
啟用net.ipv4.tcp_tw_reuse后,如果新的時間戳,比以前存儲的時間戳更大,那么linux將會從TIME-WAIT狀態的存活連接中,選取一個,重新分配給新的連接出去的TCP連接。
連出的TIME-WAIT狀態連接,僅僅1秒后就可以被重用了。

如何確通訊安全性?

TIME-WAIT的第一個作用是避免新的連接(不相關的)接收到重復的數據包。由於使用了時間戳,重復的數據包會因為timestamp過期而丟棄。
第二個作用是確保遠程端(遠程的不一定是服務端,有可能,對於服務器來說,遠程的是客戶端,我這里就用遠程端來代替)是不是在LAST-ACK狀態。因為有可能丟ACK包丟。遠程端會重發FIN包,直到

  • 放棄(連接斷開)
  • 等到ACK包
  • 收到RST包

如果 FIN包接及時收到,本地端依然是TIME-WAIT狀態,同時,ACK包也會發送出去。

一旦新的連接替換了TIME-WAIT的entry,新連接的SYN包會被忽略掉(這得感謝timestramps),也不會應答RST包,但會重傳FIN包。 FIN包將會收到一個RST包的應答(因為本地連接是SYN-SENT狀態),這會讓遠程端跳過LAST-ACK狀態。 最初的SYN包將會在1秒后重新發送,然后完成連接的建立。看起來沒有啥錯誤發生,只是延遲了一下。
last-ack-reuse
另外,當連接被重用時,TWrecycled計數器會增加的。「譯者注:見/proc/net/netstat 中TWrecycled的值」

net.ipv4.tcp_tw_recycle

這種機制也依賴時間戳選項,這也會影響到所有連接進來和連接出去的連接。「譯者注:linux上tcp_timestamps默認開啟」
TIME-WAIT狀態計划更早的過期:它將會在超時重發(RTO)間隔后移除(底層會根據當前連接的延遲狀況根據RTT來計算RTO值,上篇《PHP-FPM中backlog參數變更的一些思考》也有提到過,比較復雜的算法)。可以執行ss指令,獲取當前存活的TCP連接狀態,查看這些數據。「譯者注:linux指令ss的結果中rto,rtt值單位均為ms」

1
2
3
4
$ ss --info  sport = :2112 dport = :4057
State      Recv-Q Send-Q    Local Address:Port        Peer Address:Port  
ESTAB      0      1831936   10.47.0.113:2112          10.65.1.42:4057   
          cubic wscale:7,7 rto:564 rtt:352.5 /4 ato:40 cwnd:386 ssthresh:200 send 4.5Mbps rcv_space:5792

Linux將會放棄所有來自遠程端的timestramp時間戳小於上次記錄的時間戳(也是遠程端發來的)的任何數據包。除非TIME-WAIT狀態已經過期。

01
02
03
04
05
06
07
08
09
10
11
12
13
if (tmp_opt.saw_tstamp &&
     tcp_death_row.sysctl_tw_recycle &&
     (dst = inet_csk_route_req(sk, &fl4, req, want_cookie)) != NULL &&
     fl4.daddr == saddr &&
     (peer = rt_get_peer(( struct rtable *)dst, fl4.daddr)) != NULL) {
         inet_peer_refcheck(peer);
         if ((u32)get_seconds() - peer->tcp_ts_stamp < TCP_PAWS_MSL &&
             (s32)(peer->tcp_ts - req->ts_recent) >
                                         TCP_PAWS_WINDOW) {
                 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
                 goto drop_and_release;
         }
}

當遠程端主機HOST處於NAT網絡中時,時間戳在一分鍾之內(MSL時間間隔)將禁止了NAT網絡后面,除了這台主機以外的其他任何主機連接,因為他們都有各自CPU CLOCK,各自的時間戳。這會導致很多疑難雜症,很難去排查,建議你禁用這個選項。另外,對方上TCP的LAST-ACK狀態是體現本機net.ipv4.tcp_tw_recycle的最好數據。

總結

最合適的解決方案是增加更多的四元組數目,比如,服務器可用端口,或服務器IP,讓服務器能容納足夠多的TIME-WAIT狀態連接。在我們常見的互聯網架構中(NGINX反代跟NGINX,NGINX跟FPM,FPM跟redis、mysql、memcache等),減少TIME-WAIT狀態的TCP連接,最有效的是使用長連接,不要用短連接,尤其是負載均衡跟web服務器之間。尤其是鏈家事件中的PHP連不上redis

在服務端,不要啟用net.ipv4.tcp_tw_recycle,除非你能確保你的服務器網絡環境不是NAT。在服務端上啟用net.ipv4.tw_reuse對於連接進來的TCP連接來說,並沒有任何卵用。
在客戶端(尤其是服務器上,某服務以客戶端形式運行時,比如上面提到的nginx反代,連接着redis、mysql的FPM等等)上啟用net.ipv4.tcp_tw_reuse,還算稍微安全的解決TIME-WAIT的方案。再開啟net.ipv4.tcp_tw_recycle的話,對客戶端(或以客戶端形式)的回收,也沒有什么卵用,反而會發生很多詭異的事情(尤其是FPM這種服務器上,相對nginx是服務端,相對redis是客戶端)。

最后引用一下W. Richard Stevens在《UNIX網絡編程》的一句話

The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it.
譯者:存在即合理,勇敢面對,而不是逃避。


免責聲明!

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



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