由於TCP協議整個機制也非常復雜我只能盡可能的在某一條線上來說,不可能面面俱到,如果有疏漏或者對於內容有異議可以留言。謝謝大家。
查看服務器上各個狀態的統計數量:
netstat -ant | awk '/^tcp/ {++y[$NF]} END {for(w in y) print w, y[w]}'
單獨查看TIME_WAIT,ss -nat | grep TIME-WAIT
ss
命令中的TIME WAIT的寫法和netstat
中有所不同
TIME_WAIT的作用
主動斷開的一方的TCP連接會在這個狀態下保持2MSL,其作用就2個:
-
確保對方收到自己發送的最后一個ACK(因為對方發送了FIN),如果對方沒有收到自己發送的ACK必定會重新發送FIN,這樣保證4次斷開的完整性。因為MSL是最大報文生存時間,如果在1個MSL時間內自己發送的ACK對方沒有收到那就注定收不到了,而且對方肯定還會發送FIN,那么一個FIN發送過來的最長時間也是1個MSL,所以這里要等待2MSL。
-
另外一個原因就是避免延遲的IP報文,在頻繁短連接的場景下客戶端通常會對同一個IP和端口在短時間內發起多次連接,而客戶端使用的端口是自己系統隨機分配的高位端口,有一定概率發生上一個socket四元組和下一個socket四元組一樣,如果這時候一個原本屬於上一個socket四元組的被延遲的IP報文送達,那么這將發送數據混亂的狀態,所以為了避免這種情況就利用MSL這個報文最大生存時長機制讓殘余的IP報文在網絡中消失。這時候同樣的四元組又可以被使用了。
另外你無須擔心這個遲到的IP報文是有用的,因為TCP是可靠連接,它有重傳機制,所以這個遲到的IP報文消失不會影響之前通信的數據完整性。哪怕這個報文是在2MSL期間到達也將會被拋棄。
處於該狀態的socket什么時候可以再次使用:
-
2MSL之后
-
如果處於2MS期間,重用連接那么要保證新連接的TCP的Seq也就是序列號要比之前的大
-
如果處於2MS期間,重用的連接要保證后續的時間戳要比之前的連接的時間戳更晚
只有滿足上面的條件才不會在發生新連接上出現老連接的延遲的IP分組,滿足第一個條件則不會出現延遲的IP分組,如果滿足后面的條件那么延遲的IP分組即使出現在新連接上也會被直接丟棄進而不會影響現有通信數據。
使用Wireshark抓包在TCP中顯示的Seq都是從0開始的,這是這個軟件做了處理,如下圖:
要想看真正的序列號需要做一個配置:
Wireshark-->Preferences-->Protocols-->Tcp
點擊確定你就可以看到真實的Seq
2MSL到底有多長呢?這個不一定,1分鍾、2分鍾或者4分鍾,還有的30秒。不同的發行版可能會不同。在Centos 7.6.1810 的3.10內核版本上是60秒。v3.10/source/include/net/tcp.h
TIME_WAIT會影響什么
端口:但是這是對於通信過程中扮演客戶端角色的一端來說,因為客戶端使用隨機端口來訪問服務器,當它主動斷開的時候會出現這個狀態,比如第一次系統給它分配了一個51000的隨機端口訪問服務器,然后客戶端主動斷開了,在2MSL期間,該端口就處於TIME_WAIT狀態,如果它再次訪問相同的服務器,那么系統會為它再次分配一個隨機端口,如果51000端口還處於TIME_WAIT狀態,那么這個隨機端口就肯定不是51000,如果51000端口不處於TIME_WAIT狀態,那么這個隨機端口就有可能是51000。所以這個狀態在一定期間內對於客戶端角色來講會影響並發量,大量這個TIME_WAIT就導致可用隨機端口不斷減少。
內存:這個量會很小,無需擔心,哪怕是上萬的TIME_WAIT。
文件描述符:但是處於TIME_WAIT狀態的套接字其實是已經關閉了文件描述符,也就是說這個狀態並不占用文件描述符這也就是意味着該狀態不會對應一個打開的文件。
如何解決
網上很多人給出的答案是調整內核參數比如下面的參數,但是這些答案有很多誤區,在不同場景下並不一定適用,所以這里先對參數做一下澄清:
net.ipv4.tcp_tw_reuse = 1
表示開啟重用。允許將一個處於TIME-WAIT狀態的端口重新用於新的TCP連接,默認為0,表示關閉,其防止重復報文的原理也是時間戳,具體看后面。
net.ipv4.tcp_tw_recycle = 1
表示開啟TCP連接中TIME-WAIT sockets的快速回收,意思就是系統會保存最近一次該socket連接上的傳輸報文(包括數據或者僅僅是ACK報文)的時間戳,當相同四元組socket過來的報文的時間戳小於緩存下來的時間戳則丟棄該數據包,並回收這個socket,默認為0,表示關閉。開啟這個功能風險有點大,NAT環境可能導致DROP掉SYN包(回復RST),在NAT場景下不要使用。需要注意在Linux內核4.10版本以后該參數就已經被移除了。
net.ipv4.tcp_fin_timeout = 60
這個時間不是修改2MSL的時長,主動關閉連接的一方接收到ACK之后會進入,FIN_WAIT-2狀態,然后等待被動關閉一方發送FIN,這個時間是設置主動關閉的一方等待對方發送FIN的最長時長,默認是60秒。在這個狀態下端口是不可能被重用的,文件描述符和內存也不會被釋放,因為這個階段被動關閉的一方有可能還有數據要發送,因為對端處於CLOSE_WAIT狀態,也就是等待上層應用程序。關於這個的真實含義我希望大家清楚,而且不要調整的太小當然太大也不行,至少在3.10內核版本上這個參數不是調整的TIME_WAIT時長。我的資料查詢3.10內核變量定義和RedHat官方解釋。至於到底如何修改TIME_WAIT的時長,目前沒找到可以通過命令或者配置的形式去修改的方式。
net.ipv4.ip_local_port_range = 32768 60999
表示用於外連使用的隨機高位端口范圍,也就是作為客戶端連接其他服務的時候系統從這個范圍隨機取出一個端口來作為源端口使用來去連接對端服務器,這個范圍也就決定了最多主動能同時建立多少個外連。
net.ipv4.tcp_max_tw_buckets = 6000
同時保持TIME_WAIT套接字的最大個數,超過這個數字那么該TIME_WAIT套接字將立刻被釋放並在/var/log/message日志中打印警告信息(TCP: time wait bucket table overflow)。這個過多主要是消耗內存,單個TIME_WAIT占用內存非常小,但是多了就不好了,這個主要看內存以及你的服務器是否直接對外。
使用
net.ipv4.tcp_tw_reuse
和net.ipv4.tcp_tw_recycle
的前提是開啟時間戳net.ipv4.tcp_timestamps = 1
不過這一項默認是開啟的。
作為7層的代理的Nginx
在這種場景下首先要搞清楚哪一側產生TIME_WAIT最多。為什么要看這個,我們知道TIME_WAIT是主動關閉一方具有的狀態,但是Nginx作為7層代理對外它是服務器而對內它是客戶端(例如,相對於后端的其他Web應用比如Tomcat)。
對外側(被動連接)
ss -nat | grep "TIME-WAIT" | awk '{print $4}' | egrep -w "192.168.71.101:80|192.168.71.101:443" | wc -l
作為代理服務器對外提供的端口就是80和443,所以我們針對"TIME-WAIT"狀態來進行過濾本地地址和端口而且通過-w進行嚴格匹配2個條件就是"192.168.71.101:80|192.168.71.101:443",這樣就統計出Nginx作為服務器一方的外連TIME-WAIT的數量。
看一下抓包情況(下圖是測試環境的包,上圖是生產環境的統計)
由於在作為Web代理角色運行的時候為了提高HTTP性能所以Nginx通常會開啟Keep-alive來讓客戶端對TCP連接進行復用,如果客戶端在Keep-alive超時內沒有進行通信那么當觸發超時時服務器就會主動斷開連接,也就是上圖紅色箭頭的地方,另外一個情況就是Nginx設置對Keep-alive最大請求數量,意思是改鏈接在復用的時候可以發送多少次請求,如果到達這個最大請求次數也會斷開連接,但無論怎么說這種情況是服務器主動斷開所以TIME_WAIT則會出現在服務器上。
對於這種情況的TIME_WAIT通過修改net.ipv4.tcp_tw_reuse
無法優化,因為服務器工作在80或者443端口,不存在重復使用或者快速回收的前提。開啟net.ipv4.tcp_tw_recycle
這個功能倒是還有點意義。
對內側(主動連接)
ss -nat | grep "TIME-WAIT" | awk '{print $4}' | egrep -w -v "192.168.71.101:80|192.168.71.101:443" | wc -l
我們增加一個-v參數來取反,這樣就獲取了本地地址中不是80和443端口的TIME-WAIT狀態數量,那么這個數量就是Nginx作為客戶端進行內連后端服務器所產生的。
很明顯對內側的TIME_WAIT明顯比對外側要高,這就是因為Nginx反向代理到后端使用隨機端口來主動連接后端服務的固定端口,在短連接的情況下(通常是短連接),Nginx作為主動發起連接的一方會主動斷開,所以在業務繁忙的Nginx代理服務器上會看到大量的對內側的TIME_WAIT。
基於這種情況可以采用net.ipv4.tcp_tw_reuse
和net.ipv4.tcp_tw_recycle
優化方式,因為高位隨機端口具備復用的可能。當然至於舊IP分組影響新連接的情況在前面已經說過了其依靠時間戳來做丟棄。具體機制請看后面,現在你只需要知道是依靠時間戳來規避這個問題。
另外net.ipv4.ip_local_port_range
參數可以設置一個更大的范圍,比如net.ipv4.ip_local_port_range = 2048 65000
這就意味着你的可用隨機端口多了,端口少我們更多關注與端口復用,端口多其實是不是復用的意義就不是那么大,當然這還得取決於並發量,當然這里也不要死磕,如果你的並發量是100萬,你怎么可能指望1台Nginx來抗住流量呢,顯然需要構建Nginx集群。
再有net.ipv4.tcp_max_tw_buckets
這個參數當主機對外的時候需要調整,如果完全是內網提供服務那么這個值無需關心,它根據系統內存動態生成的,當然你可以修改。在對外的時候主要是簡單防止DoS攻擊。
net.ipv4.tcp_fin_timeout
這個值保持默認60秒或者調整成30秒都可以,主要避免對端上層應用死掉了無法進行正常發送fin,進而長期處在CLOSE_WAIT階段,這樣你自己這段的服務器就被拖住了。
總結
對於TIME_WAIT不要死磕,存在即合理,明明是一個很正常的且保證可靠通信的機制你非要抑制它的產生或者讓它快速消失。任何的調整都是雙刃劍,就像2台Nginx組成的集群去抗100萬並發的流量,你非要去優化TIME_WAIT,你為什么不想想會不會是你Nginx集群規模太小了呢?
作為不會主動進行外連的服務器來說對於TIME_WAIT除了消耗一點內存和CPU資源之外你不必過多關心這個狀態。
針對Nginx做反代的場景使用reuse優化一下,另外調大一下高位端口范圍,fin_timeout可以設置小一點,至於net.ipv4.tcp_max_tw_buckets保存默認就可以,另外對於net.ipv4.tcp_tw_recycle則放棄使用吧,比較從Linux 4.10以后這個參數也被棄用了參見kernel.org。
2MSL和resue或者recycle會不會有沖突
這個問題在TCP上有一個術語縮寫是PAWS,全名為PROTECT AGAINST WRAPPED SEQUENCE NUMBERS,也就是防止TCP的Seq序列號反轉的機制。
我們上面介紹了2MSL的作用以及減少TIME_WAIT常用措施,但是你想過沒有重用TIME_WAIT狀態的端口以及快速回收會不會引發收到該相同4元組之前的重復IP報文呢?很顯然是有可能的,那么這里就談談如何規避。通常2種辦法:
-
TCP序列號,也就是Seq位置的數字
-
時間戳,所以這也是為什么在開啟resue和recycle的時候要求開啟時間戳功能。
TCP頭中的序列號位有長度限制(32位),其最大值為2的32次方個,這就意味着它是循環使用的,也很容易在短時間內完成一個循環(序列號反轉),在1Gbps的網絡里17秒就可以完成一個循環,所以單純的通過檢查序列號不能完全實現阻擋老IP分組的數據,因為高速網絡中這個循環完成的太快,而一個IP分組的最長TTL是2MSL,通常是1分鍾,所以最主要還是靠時間戳。
前面我們也幾次提到時間戳,比如在reuse和recycle的時候提到會對比時間戳,如果收到的報文時間戳小於最近連接的時間戳就會被丟棄,那么我們如何獲取這個時間戳呢?我們先看看它長什么樣子:
TSval:發送端時間戳
TSecr:對端回顯時間戳
我們看第三行,如下圖:
這一行是客戶端回復ACK給服務器完成三次握手的最后一個階段,TSval就是客戶端的時間戳這個和第一行一樣這是因為速度快還沒有走完一個時間周期,這一行的TSecr是434971890,這個就是第二行服務器回復SYN時候給客戶端發來的服務器的時間戳,這個就叫做回顯時間戳。
這個時間戳是一個相對時間戳而不是我們通常理解的絕對時間戳(自1970年1月1日的那種形式),而且你不能把它當做時間來用,在RFC1323中也提到對報文的接收者來講時間戳可以看做另外一種高階序列號。
這里就會有一個問題,2個時間戳,一個是自己的,一個是對端的,到底用哪個時間戳來進行比較來確定是否丟棄報文呢?答案是TSval,也就是發送端的時間戳。這樣很容易理解,作為主動斷開的一方要丟棄的是對端傳遞過來的重復報文,顯然需要用對端的時間戳來判斷不可能用自己的時間戳。而且從上圖可以看到自己的時間戳和對端的時間戳明顯有很大差距,也就是說這個時間戳是通信雙方自己生成的。這個時間戳就放在TCP報文的options選項中,如下圖:
可以看到它是options,既然是選項那么就不是必須的,所以這也就是為什么當開啟reuse和recycle的時候要求開啟這個,因為不開啟則無法識別重復的IP分組。
簡單原理就是:保存該socket上一次報文的TSval時間戳,如果該socket的4元組被重復利用或者快速回收,那么假如收到了之前連接重復的報文,則比較該報文的時間戳是不是比保存的TSval小,如果小則丟棄。我這里只是簡單來說基於時間戳的機制來放置重復報文,整個的PAWS還有其他的原則,具體請查看RFC1323。
另外,由於時間戳也是通過一串數字來表示且TCP頭的時間戳長度也是32位(每個都是4byes),所以它也會出現循環,時間跳動頻率就決定了翻轉周期,那這個頻率是多少呢,RFC1312中規定建議在1ms到1s之間,這個時間間隔不同系統可能不一樣,不過這里內核選項和用戶選項的區別:
內核選項,在Linux中cat /boot/config-$(uname -r) | grep -w "CONFIG_HZ"
查看,
Jiffies是從計算機啟動到現在總共發生多少節拍數,節拍數叫做Tick,Tick是HZ的倒數,如果上所示HZ是1000,每秒發生1000次中斷,也就是1毫秒發生一次中斷,對應Tick是1ms,也就是每1毫秒Jiffies就加1。當重啟電腦的時候Jiffies重置。
用戶選項,由於用戶空間程序不能直接訪問,所以內核還提供了一個USER_HZ來讓用戶空間程序使用,固定為100,百分之一秒,也就是10毫秒。如何查看呢?getconf CLK_TCK
命令:
我們從網卡上看也是這個值cat /proc/sys/net/ipv4/neigh/ens33/locktime
如果這個間隔是1毫秒,那么時間戳反轉一次將是24.8天;如果是10毫秒就是248天,依次類推,但最大不能超過1秒。