程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 如何避免TCP的TIME_WAIT狀態

如何避免TCP的TIME_WAIT狀態

編輯:關於PHP編程

如何避免TCP的TIME_WAIT狀態


如何避免TCP的TIME_WAIT狀態
——lvyilong316

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

讓我們回憶一下,什麼是TCP TIME-WAIT狀態?如下圖

當TCP連接關閉之前,首先發起關閉的一方會進入TIME_WAIT狀態(也就是主動關閉連接的一方才會產生TIME_WAIT),另一方可以快速回收連接。可以用

ss -tan

來查看TCP 連接的當前狀態(注:ss命令要比netstat命令速度更快,並且功能更詳細,使用可參考:http://www.cnphp6.com/archives/66361

1.TIME-WAIT狀態的作用

對於TIME-WAIT狀態來說,有兩個作用。

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

如圖,序號為3的報文,由於某種原因在網絡中發生了延時(並沒有丟失),但是發送端因為超生又進行了3的重傳,縮短TIME_WAIT的時間後,延遲的SEQ3會被新建立的TCP連接接收。而如果采用正常的TIME_WAIT機制,可以保證SEQ3在網絡中消失(為什麼呢?因為TIME_WAIT的時間是2MSL,如果數據包沒有丟失則可以充分讓一個數據包在這個時間到達)。

2. 另外一個作用是,防止最後一個對FIN的ACK丟失,當最後一個ACK丟失時,遠程連接進入LAST-ACK狀態,如果沒有TIME-WAIT狀態,當遠程仍認為這個連接是有效的,則會繼續與其通訊,導致這個連接會被重新打開。當遠程收到一個SYN 時,會回復一個RST包,因為這SEQ不對,那麼新的連接將無法建立成功,報錯終止。

如果遠程因為最後一個ACK包丟失,導致停留在LAST-ACK狀態,將影響新建立具有相同四元組的TCP連接。

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

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT * state, about 60 seconds*/

#define TCP_FIN_TIMEOUTTCP_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來說,是利大於弊的。

2. TIME_WAIT狀態的影響

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

l新老連接(相同四元組)在TCP連接表中的slot復用避免;

l內核中,socket結構體的內存占用;

l額外的CPU開銷;

注:TIME_WAIT狀態的連接數可以使用:ss -tan state time-wait|wc -l,查看

2.1.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出現在服務端,問題會非常復雜,因為這裡並沒有日志記錄,也沒有計數器參考。不過,可以列出服務器上當前所有四元組連接的數量來確認。

[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

解決辦法是,增加四元組的范圍,這有很多方法去實現。(以下建議,可行性越來越小)

1)修改net.ipv4.ip_local_port_range參數,增加客戶端端口可用范圍。

2)增加服務端端口,多監聽一些端口,比如81、82、83這些,web服務器前有負載均衡,對用戶友好。

3)增加客戶端IP,尤其是作為負載均衡服務器時,使用更多IP去跟後端的web服務器通訊。

4)增加服務端IP。

5)當然了,最後的辦法是調整net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle。但千萬別這麼做,稍後再講。

2.2. 內存

保持大量的連接時,當多為每一連接多保留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狀態的socket以及正常活躍的socket。該哈希表的大小,取決於操作系統內存大小。在系統引導時,會打印出來,dmesg日志中可以看到。

dmesg | grep "TCP established hash table"

[ 0.169348] TCP established hash table entries: 65536 (order: 8, 1048576 bytes)

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

在該hash的bucket中,每個TIME-WAIT狀態的socket,對應一個tcp_timewait_sock結構體,其他狀態的socket則對應tcp_sock結構體。

  1. struct tcp_timewait_sock {
  2. struct inet_timewait_sock tw_sk;
  3. u32 tw_rcv_nxt;
  4. u32 tw_snd_nxt;
  5. u32 tw_rcv_wnd;
  6. u32 tw_ts_offset;
  7. u32 tw_ts_recent;
  8. long tw_ts_recent_stamp;
  9. };
  10. struct inet_timewait_sock {
  11. struct sock_common __tw_common;
  12. int tw_timeout;
  13. volatile unsigned char tw_substate;
  14. unsigned char tw_rcv_wscale;
  15. __be16 tw_sport;
  16. unsigned int tw_ipv6only : 1,
  17. tw_transparent : 1,
  18. tw_pad : 6,
  19. tw_tos : 8,
  20. tw_ipv6_offset : 16;
  21. unsigned long tw_ttd;
  22. struct inet_bind_bucket *tw_tb;
  23. struct hlist_node tw_death_node;
  24. };

(2)有一組叫做“death row”的鏈表,是用來終止TIME_WAIT狀態的連接(socket)的,鏈表上的連接根據TIME_WAIT的剩余時間按照由小到大排序,鏈表中的元素則直接復用hash表中的對應元素(所以沒有更多的消耗內存),即結構體inet_timewait_sock中的hlist_node tw_death_node成員,如上代碼的倒數第二行。

(3)另外一個相關結構稱為“hash table of bound ports”,即存放調用後bind函數的port即其相關參數。這個hash表的主要作用就是當需要動態綁定端口時,提供一個可用的port。這個hash所用的內存也可從系統的啟動日志中查到:

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

這個hash表的每個元素都是inet_bind_socket結構體。每個調用過bind的端口都會有一個元素。對於web服務器來說,它綁定的是80端口,其TIME-WAIT連接都是共享同一個entry的。對於連接遠程服務器的客戶端來說,他們的端口都是調用connect時隨機分配的,並不在hash表中占用元素(沒有調用過bind)。所以,和TIME_WAIT狀態有關的結構只有結構體tcp_timewait_sock和結構體inet_bind_socket。每一個TIME_WAIT狀態的連接都要消耗一個tcp_timewait_sock結構,而只有服務端的TIME_WAIT狀態采用消耗一個inet_bind_socket結構。

tcp_timewait_sock結構體的大小只有168 bytes,inet_bind_socket結構體為48bytes。所以,當服務器上有4W個連進來的連接進入TIME-WAIT狀態時,才用了10MB不到的內存。而如果作為客戶端有4W個連接到遠程的連接進入TIME-WAIT狀態時,才用了2.5MB的內存。再來看下slabtop的結果,這裡測試數據是5W個TIME-WAIT狀態的連接結果,其中4.5W是連接到遠程的連接:

$ 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狀態連接的內存占用,簡直可以無視。

2.3. CPU

那麼TIME_WAIT狀態對CPU的消耗影響如何呢?

TIME_WAIT狀態的增加也只是多占用了一些端口,使這些端口短時間內得不到釋放,但是hash的存儲結構會使系統在需要新端口時很快找到一個空閒端口,所以對CPU的開銷也不會明顯增大。

3.如何避免或者減小TIME_WAIT的影響

雖然通過以上分析,TIME_WAIT狀態對系統的影響不大,但如果你還是執意想減小這些影響,可以有以下三個方法:

l禁用socket延遲關閉;

l禁用net.ipv4.tcp_tw_reuse;

l禁用net.ipv4.tcp_tw_recycle;

(1) 禁用socket延遲關閉

通常情況當close被調用時,SOCKET需要延遲關閉(lingering),在內核buffers中的殘留數據將會發送到遠程地址,同時,socket會切換到TIME-WAIT狀態。如果禁用此選項,則調用close之後,底層也會關閉,不會將Buffers中殘留數據未發送的數據繼續發送。關於socket lingering 延遲關閉,會有以下兩種行為(具體和設置參數有關):

① close函數後,並不會在發送FIN分節,取而代之的是發送RST分節,而在buffers任何殘留數據都會被丟棄。在這種做法中,不會再有TIME-WAIT狀態的SOCKET出現。

② 如果當調用close函數後,socket發送buffer中仍然有殘留數據,此進程將會休眠,直到所有數據都發送完成並確認,或者所配置的linger計時器過期了。這個機制確保殘留數據在配置的超時時間內都發送出去。 如果數據正常發送出去,FIN包也正常發送,那麼將會轉換為TIME-WAIT狀態。其他異常情況下,則會發送RST。

(2)net.ipv4.tcp_tw_reuse

這個選項有什麼作用呢?根據名稱也能猜到,這個選項可以重用TIME_WAIT狀態下的連接。默認情況下TIME_WAIT狀態的時間是60s(linux中),而如果開啟了這個選項,當系統需要發起新的outgoing connection時,如果新的時間戳比之前TIME_WAIT連接的時間戳大的話(大於1s),則可直接復用原有的TIME_WAIT連接。即:TIME-WAIT狀態的連接,僅僅1秒後就可以被重用了。

這裡要解釋兩個術語,一個是outgoing connection,即主動發起的對外連接,也就是作為客戶端發起的連接,所以這個選項的開啟僅對客戶端起作用。另外一個是時間戳,RFC 1323實現了TCP拓展規范,以保證網絡繁忙狀態下的高可用。它定義了一個新的TCP選項–兩個四字節的timestamp fields時間戳字段,第一個是TCP發送方的當前時鐘時間戳,而第二個是從遠程主機接收到的最新時間戳。啟用net.ipv4.tcp_tw_reuse後,如果新的時間戳,比以前存儲的時間戳更大,那麼linux將會從TIME-WAIT狀態的存活連接中,選取一個,重新分配給新的連接出去的TCP連接。

那麼開啟這個選項對系統的安全性有何影響呢?我們從TIME_WAIT的兩個作用來分析,首先TIME_WAIT可以有效的防止老的分節出現在新的連接中,而時間戳選項的激活可以很大程度的避免這一點。

另外一點,如果關閉連接的最後一個ACK丟失會怎麼樣,即如果新連接復用了之前的TIME_WAIT連接,但又收到了上個連接的FIN包會如何呢?如下圖所示,會直接回復RST,並繼續原有連接的建立。

(3)net.ipv4.tcp_tw_recycle

這個選項同樣依賴時間戳選項,同樣更加選項名稱也能猜到,這個選項可以加快TIME_WAIT狀態連接的回收時間(不開啟默認是60s)。如果開啟了這個選項,則TIME_WAIT的回收時間變為一個3.5個RTO(超時重傳時間),當然這個時間是隨網絡狀態動態變化的,有RTT計算而來。這個選項對所有的TIME_WAIT狀態都有影響,包括incoming connections和 outgoing connections。所以開啟這個選項,對客戶和服務端都會產生影響。我們可以通過ss命令查看一個連接的RTO:

$ ss --info sport = :2112 dport = :4057State 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

4.總結

1. tw_reuse,tw_recycle 必須在客戶端和服務端timestamps 開啟時才管用(默認打開)

2. tw_reuse只對客戶端起作用,開啟後客戶端在1s內回收

3. tw_recycle 對客戶端和服務器同時起作用,開啟後在3.5*RTO 內回收,RTO 200ms~ 120s 具體時間視網絡狀況。

l對於客戶端

1)作為客戶端因為有端口65535問題,TIME_OUT過多直接影響處理能力,打開tw_reuse 即可解決,不建議同時打開tw_recycle,幫助不大。

2)tw_reuse 幫助客戶端1s完成連接回收,基本可實現單機6w/s請求,需要再高就增加IP數量吧。

3)如果內網壓測場景,且客戶端不需要接收連接,同時tw_recycle 會有一點點好處。

4)業務上也可以設計由服務端主動關閉連接

l對於服務端

1)打開tw_reuse無效

2)線上環境tw_recycle最好不要打開

服務器處於NAT 負載後,或者客戶端處於NAT後(這是一定的事情,基本公司家庭網絡都走NAT);公網服務打開就可能造成部分連接失敗,內網的話到時可以視情況打開;像我所在公司對外服務都放在負載後面,負載會把timestamp 選項都給關閉,所以就算打開也不起作用。

3) 服務器TIME_WAIT高怎麼辦

不像客戶端有端口限制,處理大量TIME_WAIT Linux已經優化很好了,每個處於TIME_WAIT 狀態下連接內存消耗很少,而且也能通過tcp_max_tw_buckets =262144配置最大上限,現代機器一般也不缺這點內存。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved