一般來講,在高併發的場景中,出現TIME_WAIT連線是正常現象,一旦四次握手連線關閉之後,這些連線也就隨之被系統回收了
但是在實際高併發場景中,很有可能會出現這樣的極端情況——大量的TIME_WAIT連線
TIME_WAIT狀態連線過多的危害
- TIME_WAIT 狀態下,TCP連線佔用的本地埠將一直無法釋放
- 如果TIME_WAIT連線把所有可用埠都佔完了(TCP埠數量上限是65535)而且還未被系統回收,就會出現無法向服務端建立新的socket連線的情況,此時系統幾乎停轉,任何連結都不能建立:
address already in use : connect
異常
相關原理
在遇到一個問題時,我們不但要看到其現象,更要看到問題產生背後的原理是什麼,這樣不但解決了問題,還能夠擴充自己的知識面
什麼是TIME_WAIT連線?
一般來講,客戶端(client)與服務端(server)之間的某個程序要進行通訊時,在運輸層層面來講先要透過三次握手來建立TCP連線
TCP三次握手
- 第一次握手:客戶端傳送一個SYN包給服務端,然後進入到SYN_ SENT狀態
- 第二次握手:處在監聽狀態的服務端收到客戶端的SYN包後進行回應:傳送一個ACK包給客戶端,同時傳送一個SYN包給客戶端, 然後進入到SYN_ RCVD狀態
- 第三次握手:客戶端在收到服務端的SYN包後傳送一個ACK包進行確認, 然後進入到ESTABLISHED (連線成功狀態)。服務端在收到ACK包後也進入ESTABLISHED (連線成功狀態)
通訊結束後,需要關閉連線,這時候就要透過TCP的四次揮手來進行關閉連線了
TCP四次揮手
- 第一次揮手:客戶端先傳送一個FIN包給服務端,然後進入到FIN _WAIT1 (終止等待1)狀態
- 第二次揮手:服務端收到FIN包之後對其進行回應:傳送一個ACK包給客戶端, 然後進入到close__ wait (關閉等待)狀態。這時候服務端處於半關閉狀態。
- 第三次揮手:同時服務端也請求關閉連線,傳送一個FIN包給客戶端, 然後進入LAST__ ACK (最後確認)狀態
- 第四次揮手:客戶端在收到服務端傳送的ACK包之後進入到FIN__ WAIT2 (終止等待2)狀態,對服務端發來的FIN包進行回應:傳送一個ACK包給服務端, 然後進入到TIME__WAIT (時間等待)狀態,等待2MSL (最長報文段壽命)後進入關閉狀態,服務端在收到客戶端發來的ACK包之後立即進入關閉狀態
從TCP四次揮手的過程我們可以看到,主動關閉連線的一端(注意這裡是說主動關閉連線的一端,即 client 和 server 都可以是主動關閉連線的一端)在收到對方的FIN包請求之後,傳送ACK包進行響應,這時候會處在TIME_WAIT狀態
為什麼要有TIME_WAIT狀態?
有很多同學可能不理解為什麼會有TIME_WAIT這個狀態,而且在這個狀態下還要先等待2MSL(報文最大生存時間)後才真正關閉連線
首先,TIME_WAIT狀態使得TCP全雙工連線的終止更加可靠
我們知道,網路的本質是不可靠的,四次揮手關閉TCP連線的過程中,最後一個ACK包是由主動關閉連線一端發出的(這裡我們假設是 client 進行主動關閉連線)。
而這個ACK有可能在路上丟失,使得處在LAST_ACK狀態的一端(server端)接收不到,如果接收不到,server 就會超時重傳 FIN 請求
所以 client 需要處在TIME_WAIT狀態並等待2MSL時間來處理 server 重傳的 FIN 請求,來使得 server 能夠正常關閉
其次,TIME_WAIT狀態的存在可以處理延遲到達的報文
網路的本質是不可靠的,也就意味著TCP報文有可能會延遲到達,TIME_WAIT狀態時,兩端的埠不能使用,要等到2MSL時間結束後才可以繼續使用,並且在等待2MSL時間的過程中,任何遲到的報文都將被丟棄
這樣就可以避免延遲到達的TCP報文被誤認為是新TCP連線的資料,並且使得這些延遲報文在網路上消失
如何檢視TIME_WAIT連線?
以我本地虛擬機器(CentOS7)為例:
檢視狀態為TIME_WAIT的TCP連線
$ netstat -tan |grep TIME_WAIT
統計TCP各種狀態的連線數
$ netstat -n | awk '/^tcp/ {++S[$NF]} END {for(i in S) print i, S[i]}'
如何最佳化
前面我們講過,出現一定數量的TIME_WAIT連線是正常現象,但是線上上生產環境面對高併發場景時可能會出現極端的情況——大量的TIME_WAIT連線
大量的TIME_WAIT連線會佔用系統本地埠,導致不能再建立新的TCP連線
那麼我們要怎麼進行最佳化呢?
大量的TIME_WAIT連線存在,其本質原因是什麼?
1.大量的短連線存在
在HTTP/1.0協議中預設使用短連線。
也就是說,瀏覽器和伺服器每進行一次HTTP操作,就會建立一次連線,任務結束後就會斷開連線,而斷開連線這個請求是由server去發起的,主動關閉連線請求一端才會有TIME_WAIT狀態連線
2.HTTP請求頭裡connection值被設定為close
如果HTTP請求中,connection的值被設定成close,那就相當於告訴server:server執行完HTTP請求後去主動關閉連線
最佳化
客戶端層面
我們可以在客戶端將HTTP請求頭裡connection的值設定為:keep-alive。將短連線改成長連線
長連線比短連線從根本上減少了server去主動關閉連線的次數,減少了TIME_WAIT狀態連線的產生
(在利用nginx做反向代理時,如果要設定成長連線,則需要設定成:1.從client到nginx的連線是長連線。2.從nginx到server的連線是長連線)
伺服器層面
我們可以透過修改伺服器的系統核心引數來進行最佳化
1.允許將TIME_WAIT狀態的socket重新用於新的TCP連線
這樣的好處就是如果出現大量TIME_WAIT狀態的連線,也能夠將這些連線佔用的埠重新用於新的TCP連線
$ vim /etc/sysctl.confnet.ipv4.tcp_tw_reuse = 1 #預設為0,表示關閉
2.快速回收TIME_WAIT狀態的socket
$ vim /etc/sysctl.confnet.ipv4.tcp_tw_recycle = 1#預設為0,表示關閉
3.將MSL值縮減
linux中MSL的值預設為60s,我們可以透過縮減MSL值來使得主動關閉連線一端由TIME_WAIT狀態到關閉狀態的時間減少
但是這樣做會導致延遲報文無法清除以及主動關閉連線一端不能收到重傳來的FIN請求,也會影響很多基於TCP的應用的連線複用和調優
所以在實際生產環境中,需要謹慎操作
#檢視預設的MSL值
$cat /proc/sys/net/ipv4/tcp_fin_timeout
#修改
$echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout
或者
$ vim /etc/sysctl.conf
net.ipv4.tcp_fin_timeout = 30