本文爲翻譯英文BLOG《Coping with the TCP TIME-WAIT state on busy Linux servers》,但並不是完整的翻譯,譯者CFC4N對原文理解後,進行了調整,增長了相關論點論據,跟原文稍有不一樣。翻譯的目的,是爲了加深本身知識點的記憶,以及分享給其餘朋友,或許對他們也有幫助。文章比較長,沒耐心請點關閉。php
linux 內核文檔中,對net.ipv4.tcp_tw_recycle的描述並非很明確。html
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,手冊裏稍微有點描述,以下:node
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鏈接上來講,是安全的。(用於客戶端時的配置)mysql
這裏的註釋說明很是的少,咱們發現,網上不少linux參數調整指南都建議把這些參數net.ipv4.tcp_tw_recycle 設置1「啓用」,用於快速減小在TIME-WAIT狀態TCP鏈接數。
可是,在TCP(7)手冊中,參數net.ipv4.tcp_tw_recycle 很是蛋疼,尤爲是在普通用戶家中,有多臺設備,或者網吧、公司等多臺設備,共用同一個NAT設備環境下,TW回收選項是頗有問題的面向公共服務器做爲它不會把手鍊接兩臺不一樣的計算機上,這問題很難發現,無從下手。linux
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鏈接創建錯誤。若是沒有技術大神的指點的話,千萬不要去改動他。nginx
下文將給予更詳細的解釋,但願能夠糾正互聯網上錯誤的觀點,尤爲是轉載比較多的內容,搜索時,每每排在前面,使用者每每接觸到的都是不嚴謹的或者是錯誤的知識點。git
正如此文,在 net.ipv4.tcp_tw_recycle 控制參數中,儘管不少地方寫的是ipv4,但對ipv6一樣實用。此外,咱們這裏聊的是Linux TCP協議棧,在linux上可能會受到Netfilter影響,稍微有差別。程序員
讓咱們回憶一下,什麼是TCP TIME-WAIT狀態?以下圖
web
這圖中的流程不是很好理解,再看一張流程更清晰的圖
當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狀態來講,有兩個做用
1、人盡皆知的是,防止上一個TCP鏈接的延遲的數據包(發起關閉,但關閉沒完成),被接收後,影響到新的TCP鏈接。(惟一鏈接確認方式爲四元組:源IP地址、目的IP地址、源端口、目的端口),包的序列號也有必定做用,會減小問題發生的概率,但沒法徹底避免。尤爲是較大接收windows size的快速(回收)鏈接。RFC1137解釋了當TIME-WAIT狀態不足時將會發生什麼。若是TIME-WAIT狀態鏈接沒有被快速回收,會避免什麼問題呢?請看下面的例子:
縮短TIME-WAIT的時間後,延遲的TCP 包會被新創建的TCP鏈接接收。
2、另一個做用是,當最後一個ACK丟失時,遠程鏈接進入LAST-ACK狀態,它能夠確保遠程已經關閉當前TCP鏈接。若是沒有TIME-WAIT狀態,當遠程仍認爲這個鏈接是有效的,則會繼續與其通信,致使這個鏈接會被從新打開。當遠程收到一個SYN 時,會回覆一個RST包,由於這SEQ不對,那麼新的鏈接將沒法創建成功,報錯終止。
若是遠程由於最後一個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來講,是利大於弊的。
咱們來看下,爲何這個狀態能影響到一個處理大量鏈接的服務器,從下面三個方面來講:
而ss -tan state time-wait|wc -l的結果,並不能說明這些問題。
處於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.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,對於三種不一樣的做用,有三個不一樣的結構。
1、「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
};
|
2、有一組叫作「death row」的鏈接列表,是用來終止TIME-WAIT狀態的鏈接的,這會在他們過時以前,開始申請。它佔用的內存空間,跟在鏈接哈希表中的同樣。這個結構體hlist_node tw_death_node是inet_timewait_sock的一個成員,如上代碼的倒數第二行。
3、有個綁定端口的哈希表,存儲綁定端口跟其餘參數,用來確保當前端口沒有被使用的,好比在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這邊,查找一個空閒端口的操做,仍是蠻珍貴的。這由inet_csk_get_port() 函數,加鎖,遍歷整個空閒端口列表實現。這個哈希表裏條目數量大一般不是問題,若是服務器上存在大量鏈接到遠程TIME-WAIT狀態的鏈接(好比FPM連redis、memcache之類),都會同享相同的profile,這個特性會很是快的按照順序找到一個新的空閒端口。
若是你讀了上面的章節後,仍對TIME-WAIT狀態的鏈接存有疑問,那麼接着看吧:
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),比較合適。一樣,也有不少無可厚非的理由不能禁用它。
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包,直到
若是 FIN包接及時收到,本地端依然是TIME-WAIT狀態,同時,ACK包也會發送出去。
一旦新的鏈接替換了TIME-WAIT的entry,新鏈接的SYN包會被忽略掉(這得感謝timestramps),也不會應答RST包,但會重傳FIN包。 FIN包將會收到一個RST包的應答(由於本地鏈接是SYN-SENT狀態),這會讓遠程端跳過LAST-ACK狀態。 最初的SYN包將會在1秒後從新發送,而後完成鏈接的創建。看起來沒有啥錯誤發生,只是延遲了一下。
另外,當鏈接被重用時,TWrecycled計數器會增長的。「譯者注:見/proc/net/netstat 中TWrecycled的值」
這種機制也依賴時間戳選項,這也會影響到全部鏈接進來和鏈接出去的鏈接。「譯者注: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.
譯者:存在即合理,勇敢面對,而不是逃避。
在linux 4.1內核中,net.ipv4.tcp_tw_recycle參數將被移除。net.ipv4.tcp_tw_recycle has been removed from Linux 4.12.
The tcp_tw_recycle was already broken for connections
behind NAT, since the per-destination timestamp is not
monotonically increasing for multiple machines behind
a single destination address.After the randomization of TCP timestamp offsets
in commit 8a5bd45f6616 (tcp: randomize tcp timestamp offsets
for each connection), the tcp_tw_recycle is broken for all
types of connections for the same reason: the timestamps
received from a single machine is not monotonically increasing,
anymore.Remove tcp_tw_recycle, since it is not functional. Also, remove
the PAWSPassive SNMP counter since it is only used for
tcp_tw_recycle, and simplify tcp_v4_route_req and tcp_v6_route_req
since the strict argument is only set when tcp_tw_recycle is
enabled.
關注微信公衆號,手機閱讀更方便: 程序員的閱微草堂
莿鳥棲草堂 由 CFC4N 創做,採用 知識共享 署名-非商業性使用-相同方式共享(3.0未本地化版本)許可協議進行許可。基於http://www.cnxct.com上的做品創做。轉載請註明轉自:不要在linux上啓用net.ipv4.tcp_tw_recycle參數