1. 程式人生 > >TCP的TIME WAIT快速回收與重用

TCP的TIME WAIT快速回收與重用

宣告一點:

Linux中是無法修改tcp的TIME_WAIT值的,除非重新編譯,起碼我是沒有找到怎麼改。值得注意的是,net.ipv4.tcp_fin_timeout這個引數是FIN_WAIT_2的值,而不是TIME_WAIT的值。我不知道為何很多人都會把它當成是TIME_WAIT的值,想了一下,我覺得是兩點:
1.TIME_WAIT過於耀眼,以至於所有出現timeout,加上裡面有個tcp的配置,都會想當然往TIME_WAIT上聯絡;
2.FIN_WAIT_2過於默默無聞,以至於很少有人知道它也是一種狀態。

所以,我想大家在學習的時候,不能想當然。

TIME_WAIT的作用

TCP初見於網際網路早期,當時的網路很不穩定,大量的丟包,為了冗餘,大量包複製多徑傳輸,網速慢--正因為如此,TCP才會更有意義。為了保證TCP的嚴格語義,就要避免上述冗餘機制以及網速慢導致的問題,這些問題集中體現在連線關閉時的四次揮手上。
        由於TCP是全雙工的,因此關閉連線必須在兩個方向上分別進行。首先發起關閉的一方為主動關閉方,另一方為被動關閉方。很多人都會在這裡暈掉,實際上四次揮手比三次握手還簡單。四次揮手簡單地分為三個過程:
過程一.主動關閉方傳送FIN,被動關閉方收到後傳送該FIN的ACK;
過程二.被動關閉方傳送FIN,主動關閉方收到後傳送該FIN的ACK;
過程三.被動關閉方收到ACK。

以上三步下來,圓圈就閉合了!也就是說,在過程三後,被動關閉方就可以100%確認連線已經關閉,因此它便可以直接進入CLOSE狀態了,然而主動關閉的一方,它無法確定最後的那個發給被動關閉方的ACK是否已經被收到,據TCP協議規範,不對ACK進行ACK,因此它不可能再收到被動關閉方的任何資料了,因此在這裡就陷入了僵局,TCP連線的主動關閉方如何來保證圓圈的閉合?這裡,協議外的東西起作用了,和STP(Spanning tree)依靠各類超時值來收斂一樣,IP也有一個超時值,即MSL,這類超時值超級重要,因為它們給出了一個物理意義上的不可逾越的界限,它們是自洽協議的唯一外部輸入。MSL表明這是IP報文在地球上存活的最長時間,如果在火星上,Linux的程式碼必須要重新定義MSL的值並且要重新編譯。
       於是問題就解決了,主動關閉一方等待MSL時間再釋放連線,這個狀態就是TIME_WAIT。對於被動關閉的一方,發出FIN之後就處在了LAST_ACK狀態了,既然已經發出FIN了,缺的無非也就是個ACK,連線本身其實已經關閉了,因此被動關閉的一方就沒有TIME_WAIT狀態。
       實際上,兩倍MSL才能說明一個報文的徹底丟失,因為還要記入其ACK返回時的MSL。

TIME_WAIT的問題

這個就不多說了,由於TIME_WAIT的存在,短連線時關閉的socket會長時間佔據大量的tuple空間。

TIME_WAIT的快速回收

Linux實現了一個TIME_WAIT狀態快速回收的機制,即無需等待兩倍的MSL這麼久的時間,而是等待一個Retrans時間即釋放,也就是等待一個重傳時間(一般超級短,以至於你都來不及能在netstat -ant中看到TIME_WAIT狀態)隨即釋放。釋放了之後,一個連線的tuple元素資訊就都沒有了,而此時,新建立的TCP卻面臨著危險,什麼危險呢?即:
1.可能被之前遲到的FIN包給終止的危險;
2.被之前連線劫持的危險;
...

於是需要有一定的手段避免這些危險。什麼手段呢?雖然曾經連線的tuple資訊沒有了,但是在IP層還可以儲存一個peer資訊,注意這個資訊不單單是用於TCP這個四層協議的,路由邏輯也會使用它,其欄位包括但不限於:
對端IP地址
peer最後一次被TCP觸控到的時間戳

...
在快速釋放掉TIME_WAIT連線之後,peer依然保留著。丟失的僅僅是埠資訊。不過有了peer的IP地址資訊以及TCP最後一次觸控它的時間戳就足夠了,TCP規範給出一個優化,即一個新的連線除了同時觸犯了以下幾點,其它的均可以快速接入,即使它本應該處在TIME_WAIT狀態(但是被即快速回收了):
1.來自同一臺機器的TCP連線攜帶時間戳;
2.之前同一臺peer機器(僅僅識別IP地址,因為連線被快速釋放了,沒了埠資訊)的某個TCP資料在MSL秒之內到過本機;
3.新連線的時間戳小於peer機器上次TCP到來時的時間戳,且差值大於重放視窗戳。

看樣子只有以上的3點的同時滿足才能拒絕掉一個新連線,要比TIME_WAIT機制設定的障礙導致的連線拒絕機率小很多,但是要看到,上述的快速釋放機制沒有埠資訊!這就把機率擴大了65535倍。然而,如果對於單獨的機器而言,這不算什麼,因此單臺機器的時間戳不可能倒流的,出現上述的3點均滿足時,一定是老的重複資料包又回來了。
        但是,一旦涉及到NAT裝置,就悲催了,因為NAT裝置將資料包的源IP地址都改成了一個地址(或者少量的IP地址),但是卻基本上不修改TCP包的時間戳,這就帶來了問題。 假設PC1和PC2均啟用了TCP時間戳,它們經過NAT裝置N1往伺服器S1的22埠連線:
PC1:192.168.100.1
PC2:192.168.100.2
N1外網口(即NAT後的地址):172.16.100.1
S1:172.16.100.2
所有涉事機器的配置:
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps = 1
TCP的時間戳是根據本機的jiffers,uptime計算的,現在我能保證PC2的時間戳肯定遠小於PC1。現在在PC1上先做一個telnet:
telnet 172.16.100.2 22
連線成功,S1上抓包,得到時間戳timestamps:TS val 698583769
為了讓S1主動關閉進而快速回收TIME_WAIT,在S1上執行:
kill $(ps -ef|grep [s]sh|grep acce|awk -F ' ' '{print $2}');
目的是把僅僅光完成三次握手的連線終止掉而不觸動已經連線的ssh。此時馬上在PC2上telnet:
telnet 172.16.100.2 22
不通!在S1上抓包,得到時間戳timestamps:TS val 27727766。明顯小於PC1的!由於有NAT裝置,S1看來是同一臺機器發出的,且出現了時間戳倒流,連線拒絕!此時在S1上檢視計數值:
cat /proc/net/netstat
發現了PAWSPassive對應的值增加了1,每次PC2重發SYN,該計數值均會增加1,直到一個MSL時間過後,才能連線成功。如果反過來就沒有問題,即先在PC2上telnet,然後S1主動關閉,然後緊接著PC1上telnet依然可以成功,這是因為時間戳是遞增的,不滿足上述的第三點。

       僅僅兩臺機器就出現了這個問題,試問如果大量的源端機器在伺服器的入口處遇到了NAT裝置會怎樣?即一臺三層NAT裝置部署在高負載網站的入口處...沒有誰能保證時間戳小的機器一定先發起連線,各個機器頻繁連線斷開後依然按照時間戳從小到大的順序連線!!
       TIME_WAIT快速回收在Linux上通過net.ipv4.tcp_tw_recycle啟用,由於其根據時間戳來判定,所以必須開啟TCP時間戳才有效。 建議:如果前端部署了三/四層NAT裝置,儘量關閉快速回收,以免發生NAT背後真實機器由於時間戳混亂導致的SYN拒絕問題。

TIME_WAIT重用

如果說TIME_WAIT(輸入法切換太煩人了,後面簡稱TW)回收只是一種特定系統的優化實現的話,那麼TW重用則有相關的規範,即:如果能保證以下任意一點,一個TW狀態的四元組(即一個socket連線)可以重新被新到來的SYN連線使用:
1.初始序列號比TW老連線的末序列號大
2.如果使能了時間戳,那麼新到來的連線的時間戳比老連線的時間戳大
Linux上完美實現了上述的特性,可以通過下面的實驗來證實:
S1上的服務程式:偵聽埠1234,accept新連線,傳送一段資料後呼叫close主動關閉連線。
S1上的額外配置:通過iptables禁止RESET包進入第四層,因為它會將TW狀態終結。
PC1上客戶端程式:繫結192.168.100.1,2000埠,連線S1,獲取資料後呼叫close關閉連線。
PC2上客戶端程式:使用IP_TRANSPARENT選項同樣繫結192.168.100.1地址和2000埠,其它和PC1的程式相同。
啟動服務端S1:172.16.100.2,不斷偵聽埠1234;
啟動PC1上的C1:192.168.100.1,埠2000,連線S1的1234埠;
此時在S1上抓包,獲取正常的三次握手/資料傳輸/四次揮手資料包。此時在S1上netstat -ant可以看到一個TW狀態的連線。
啟動PC2上的C2:192.168.100.1,埠2000,連線S1的1234埠;
此時在S1上抓包,SYN序列號seq 3934898078大於PC1發起連線時的最後一個序列號[F.], seq 2513913083, ack 3712390788,S1正常回復SYNACK:Flags [S.], seq 3712456325, ack 3934898079, ...對於這種TW重用的情況,S1的SYNACK的初始序列號是通過TW狀態老連線的最後一個ack,即3712390788,加上常量65535+2算出來的!
      以上的實驗是在關閉時間戳的情況下完成的,實際上開啟時間戳的話,重用的可能性更高一些,畢竟是否能重用一個TW連線是通過以上的條件之一來判斷的!

從外部幹掉TIME_WAIT

TIME_WAIT狀態時則一個闌尾!Linux系統上,除了使能recycle tw,在Linux系統上你無法更簡單地縮短TW狀態的時間,但是80%的問題卻都是由TW狀態引發,在Windows系統上,你需要在登錄檔新增一個隱式的項,稍微拼寫錯誤都會引發沉默的失敗!TW確實讓人生氣,因此我一直都希望幹掉TW狀態的連線,特別是幹掉服務端TW狀態的連線!我們可以通過TCP的RESET來乾死TW連線。這個怎麼說呢?
       根據TCP規範,收到任何的傳送到未偵聽埠或者序列號亂掉(視窗外)的資料,都要回執以RESET,這就是可以利用的!一個連線等待在TW,它自身無能為力,但是可以從外部間接殺掉它!具體來講就是利用了IP_TRANSPARENT這個socket選項,它可以bind不屬於本地的地址,因此可以從任意機器繫結TW連線的peer地址以及埠,然後發起一個連線,TW連線收到後由於序列號亂序會直接傳送一個ACK,該ACK會回到TW連線的peer處,由於99%的可能該peer已經釋放了連線(對端由於不能收到FIN-ACK的ACK,進而不放心ACK是否已經到達對端,等待MSL以便所有的老資料均已經丟失),因此peer由於沒有該連線會回覆RESET,TW連線收到RESET後會釋放連線,進而為後續的連線騰出地方!

Linux實現Tips

Linux照顧到了一種特殊情況,即殺死程序的情況,在系統kill程序的時候,會直接呼叫連線的close函式單方面關閉一個方向的連線,然後並不會等待對端關閉另一個方向的連線程序即退出。現在的問題是,TCP規範和UNIX程序的檔案描述符規範直接衝突!程序關閉了,套接字就要關閉,但是TCP是全雙工的,你不能保證對端也在同一個時刻同意並且實施關閉動作,既然連線不能關閉,作為檔案描述符,程序就不會關閉得徹底!所以,Linux使用了一種“子狀態”的機制,即在程序退出的時候,單方面傳送FIN,然後不等後續的關閉序列即將連線拷貝到一個佔用資源更少的TW套接字,狀態直接轉入TIMW_WAIT,此時記錄一個子狀態FIN_WAIT_2,接下來的套接字就和原來的屬於程序描述符的連線沒有關係了。等到新的連線到來的時候,直接匹配到這個主狀態為TW,子狀態為FIN_WAIT_2的TW連線上,它負責處理FIN,FIN ACK等資料。

TIME_WAIT快速回收與重用

通過以上描述,我們看到TW狀態的連線既可以被快速回收又可以被重用,但是二者的副作用是不同的。對於快速回收,由於丟失了TW連線的埠資訊,全部對映到了IP地址資訊,所以整個IP地址,也就是整機均被列入了考察物件,這本身並沒有什麼問題,因為快速回收只考慮時間戳資訊,只要其保持單調遞增即可,一般的機器時間是不會倒流的,但是遇到NAT合併就不行了,NAT裝置為所有的內部裝置代理一個IP地址即主機標識,然而卻不觸動其時間戳,而各個機器的時間戳並不滿足任何規律...
        TW重用解決了整機範圍拒絕接入的問題,但是卻面臨資源消耗的問題。它這個做法的依據之一仍然為,一般一個單獨的主機是不可能在MSL內用同一個埠連線同一個服務的,除非它做了bind。因此等待一些遺留的資料丟失或者到達是有盼頭的。有一點我有異議,我個人感覺,如果處在默默地TW等待中,有默默地非遞增SYN或者遞增時間戳SYN到來,千萬別發ACK,只要默默丟棄即可,因為你發了ACK,對方在已經終止了連線的情況下,就會發RESET,進而終止掉本段連線。

TIME_WAIT的80/20悲劇

80%的問題都由20%的TW引發,甚至在各種的TCP實現中,大量的程式碼在處理TW!我個人覺得這有點過了!引入TW狀態是為了確認老資料到來或者消失,且等待時延那麼久,這已經是很多年以前的事了,那時我可能剛出生,家裡可能還沒有裝電話...那時的網路條件,引入這些機制是確實需要的,但是隨著網路技術的發展,TW已經慢慢成了雞肋。即便新的TCP連線被老的FIN終止又怎樣,即使新的連線被老的劫持又能怎樣,即便不考慮這些,MSL未免也太長了些吧,話說當年DDN年代,這個值就已經很久了...不要試圖保持TCP的安全了,即使面對中間人又能怎樣?我們不是可以用SSL嗎?TCP作為一種底層的傳輸協議,一定要簡單,可是現在呢?雖然其核心保持著原汁原味,但是其細節使多少求知若渴的人踱步門外啊,不得不說,TCP的細節太複雜了,即使是再好的作家,也無法寫出一本讓人徹底明白的關於TCP細節的書。
        看看規範,各種公式,各種不可插拔的演算法,各種魔術字,即使作者本人估計都很難說清楚內中細節。不得不說,TCP有點過度設計了,作為當年的設計精品,在當今越發往上層移動的年代,不合適了。如今越來越多的協議或者開元軟體使用簡單的UDP做擴充套件,在實現按序到達,確認,否認,時間戳,可靠連線等機制中實現自己需要的而不是所有,從TLS到OpenVPN,無一沒有把UDP當成下一代的天驕。我很討厭TCP,很討厭這種亂七八糟的東西。你可能會反駁我,但我覺得你被洗腦了,你要知道,如果讓你設計一個可靠的有連線協議,你可能做的真的比TCP更好。

再分享一下我老師大神的人工智慧教程吧。零基礎!通俗易懂!風趣幽默!希望你也加入到我們人工智慧的隊伍中來!http://www.captainbed.net