https://draveness.me/whys-the-design-tcp-time-wait/
為什么這么設(shè)計(Why’s THE Design)是一系列關(guān)于計算機領(lǐng)域中程序設(shè)計決策的文章,我們在這個系列的每一篇文章中都會提出一個具體的問題并從不同的角度討論這種設(shè)計的優(yōu)缺點、對具體實現(xiàn)造成的影響。
在這個系列前面的文章中,我們已經(jīng)多次討論 TCP 協(xié)議的設(shè)計原理,其中包括 TCP 協(xié)議的 三次握手、流量控制和重傳機制、最大數(shù)據(jù)段 以及 粘包 等問題。本文將繼續(xù)分析 TCP 協(xié)議的實現(xiàn)細(xì)節(jié),今天要分析的問題是為什么 TCP 協(xié)議需要 TIME_WAIT 狀態(tài)以及該狀態(tài)的作用究竟是什么。
TCP 協(xié)議中包含 11 種不同的狀態(tài),TCP 連接會根據(jù)發(fā)送或者接收到的消息轉(zhuǎn)換狀態(tài),如下圖所示的狀態(tài)機展示了所有可能的轉(zhuǎn)換,其中不僅包含了正常情況下的狀態(tài)轉(zhuǎn)換過程,還包含了異常狀態(tài)下的狀態(tài)轉(zhuǎn)換:

為什么這么設(shè)計(Why’s THE Design)是一系列關(guān)于計算機領(lǐng)域中程序設(shè)計決策的文章,我們在這個系列的每一篇文章中都會提出一個具體的問題并從不同的角度討論這種設(shè)計的優(yōu)缺點、對具體實現(xiàn)造成的影響。
在這個系列前面的文章中,我們已經(jīng)多次討論 TCP 協(xié)議的設(shè)計原理,其中包括 TCP 協(xié)議的 三次握手、流量控制和重傳機制、最大數(shù)據(jù)段 以及 粘包 等問題。本文將繼續(xù)分析 TCP 協(xié)議的實現(xiàn)細(xì)節(jié),今天要分析的問題是為什么 TCP 協(xié)議需要 TIME_WAIT 狀態(tài)以及該狀態(tài)的作用究竟是什么。
TCP 協(xié)議中包含 11 種不同的狀態(tài),TCP 連接會根據(jù)發(fā)送或者接收到的消息轉(zhuǎn)換狀態(tài),如下圖所示的狀態(tài)機展示了所有可能的轉(zhuǎn)換,其中不僅包含了正常情況下的狀態(tài)轉(zhuǎn)換過程,還包含了異常狀態(tài)下的狀態(tài)轉(zhuǎn)換:

圖 1 - TCP 協(xié)議狀態(tài)
使用 TCP 協(xié)議通信的雙方會在關(guān)閉連接時觸發(fā) TIME_WAIT 狀態(tài),關(guān)閉連接的操作其實是告訴通信的另一方自己沒有需要發(fā)送的數(shù)據(jù),但是它仍然保持了接收對方數(shù)據(jù)的能力,一個常見的關(guān)閉連接過程如下1:
- 當(dāng)客戶端沒有待發(fā)送的數(shù)據(jù)時,它會向服務(wù)端發(fā)送 FIN 消息,發(fā)送消息后會進入 FIN_WAIT_1 狀態(tài);
- 服務(wù)端接收到客戶端的 FIN 消息后,會進入 CLOSE_WAIT 狀態(tài)并向客戶端發(fā)送 ACK 消息,客戶端接收到 ACK 消息時會進入 FIN_WAIT_2 狀態(tài);
- 當(dāng)服務(wù)端沒有待發(fā)送的數(shù)據(jù)時,服務(wù)端會向客戶端發(fā)送 FIN 消息;
- 客戶端接收到 FIN 消息后,會進入 TIME_WAIT 狀態(tài)并向服務(wù)端發(fā)送 ACK 消息,服務(wù)端收到后會進入 CLOSED 狀態(tài);
- 客戶端等待兩個最大數(shù)據(jù)段生命周期(Maximum segment lifetime,MSL)2的時間后也會進入 CLOSED 狀態(tài);

從上述過程中,我們會發(fā)現(xiàn) TIME_WAIT 僅在主動斷開連接的一方出現(xiàn),被動斷開連接的一方會直接進入 CLOSED 狀態(tài),進入 TIME_WAIT 的客戶端需要等待 2 MSL 才可以真正關(guān)閉連接。TCP 協(xié)議需要 TIME_WAIT 狀態(tài)的原因和客戶端需要等待兩個 MSL 不能直接進入 CLOSED 狀態(tài)的原因是一樣的3:
- 防止延遲的數(shù)據(jù)段被其他使用相同源地址、源端口、目的地址以及目的端口的 TCP 連接收到;
- 保證 TCP 連接的遠(yuǎn)程被正確關(guān)閉,即等待被動關(guān)閉連接的一方收到 FIN 對應(yīng)的 ACK 消息;
上述兩個原因都相對比較簡單,我們來展開介紹這兩個原因背后可能存在的一些問題。
阻止延遲數(shù)據(jù)段
每一個 TCP 數(shù)據(jù)段都包含唯一的序列號,這個序列號能夠保證 TCP 協(xié)議的可靠性和順序性,在不考慮序列號溢出歸零的情況下,序列號唯一是 TCP 協(xié)議中的重要約定,一旦違反了這條規(guī)則,就可能造成令人困惑的現(xiàn)象和結(jié)果。為了保證新 TCP 連接的數(shù)據(jù)段不會與還在網(wǎng)絡(luò)中傳輸?shù)臍v史連接的數(shù)據(jù)段重復(fù),TCP 連接在分配新的序列號之前需要至少靜默數(shù)據(jù)段在網(wǎng)絡(luò)中能夠存活的最長時間,即 MSL4:
To be sure that a TCP does not create a segment that carries a sequence number which may be duplicated by an old segment remaining in the network, the TCP must keep quiet for a maximum segment lifetime (MSL) before assigning any sequence numbers upon starting up or recovering from a crash in which memory of sequence numbers in use was lost.

在如上圖所示的 TCP 連接中,服務(wù)端發(fā)送的 SEQ = 301 消息由于網(wǎng)絡(luò)延遲直到 TCP 連接關(guān)閉后也沒有收到;當(dāng)使用相同端口號的 TCP 連接被重用后,SEQ = 301 的消息才發(fā)送到客戶端,然而這個過期的消息卻可能被客戶端正常接收,這就會帶來比較嚴(yán)重的問題,所以我們在調(diào)整 TIME_WAIT 策略時要非常謹(jǐn)慎,必須清楚自己在干什么。
RFC 793 中雖然指出了 TCP 連接需要在 TIME_WAIT 中等待 2 倍的 MSL,但是并沒有解釋清楚這里的兩倍是從何而來,比較合理的解釋是 — 網(wǎng)絡(luò)中可能存在來自發(fā)起方的數(shù)據(jù)段,當(dāng)這些發(fā)起方的數(shù)據(jù)段被服務(wù)端處理后又會向客戶端發(fā)送響應(yīng),所以一來一回需要等待 2 倍的時間5。
RFC 793 文檔將 MSL 的時間設(shè)置為 120 秒,即兩分鐘,然而這并不是一個經(jīng)過嚴(yán)密推斷的數(shù)值,而是工程上的選擇,如果根據(jù)服務(wù)歷史上的經(jīng)驗要求我們改變操作系統(tǒng)的設(shè)置,也是沒有任何問題的;實際上,較早版本的 Linux 就開始將 TIME_WAIT 的等待時間 TCP_TIMEWAIT_LEN 設(shè)置成 60 秒,以便更快地復(fù)用 TCP 連接資源:
在 Linux 上,客戶端的可以使用端口號 32,768 ~ 61,000,總共 28,232 個端口號與遠(yuǎn)程服務(wù)器建立連接,應(yīng)用程序可以在將近 3 萬的端口號中任意選擇一個:
但是如果主機在過去一分鐘時間內(nèi)與目標(biāo)主機的特定端口創(chuàng)建的 TCP 連接數(shù)超過 28,232,那么再創(chuàng)建新的 TCP 連接就會發(fā)生錯誤,也就是說如果我們不調(diào)整主機的配置,那么每秒能夠建立的最大 TCP 連接數(shù)為 ~470
保證連接關(guān)閉
從 RFC 793 對 TIME_WAIT 狀態(tài)的定義中,我們可以發(fā)現(xiàn)該狀態(tài)的另一個重要作用,等待足夠長的時間以確定遠(yuǎn)程的 TCP 連接接收到了其發(fā)出的終止連接消息 FIN 對應(yīng)的 ACK:
TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.
如果客戶端等待的時間不夠長,當(dāng)服務(wù)端還沒有收到 ACK 消息時,客戶端就重新與服務(wù)端建立 TCP 連接就會造成以下問題 — 服務(wù)端因為沒有收到 ACK 消息,所以仍然認(rèn)為當(dāng)前連接是合法的,客戶端重新發(fā)送 SYN 消息請求握手時會收到服務(wù)端的 RST 消息,連接建立的過程就會被終止。

這樣就違背了tcp可靠性?
在默認(rèn)情況下,如果客戶端等待足夠長的時間就會遇到以下兩種情況:
- 服務(wù)端正常收到了 ACK 消息并關(guān)閉當(dāng)前 TCP 連接;
- 服務(wù)端沒有收到 ACK 消息,重新發(fā)送 FIN 關(guān)閉連接并等待新的 ACK 消息
只要客戶端等待 2 MSL 的時間,客戶端和服務(wù)端之間的連接就會正常關(guān)閉,新創(chuàng)建的 TCP 連接收到影響的概率也微乎其微,保證了數(shù)據(jù)傳輸?shù)目煽啃浴?/p>
總結(jié)
在某些場景下,60 秒的等待銷毀時間確實是難以接受的,例如:高并發(fā)的壓力測試。當(dāng)我們通過并發(fā)請求測試遠(yuǎn)程服務(wù)的吞吐量和延遲時,本地就可能產(chǎn)生大量處于 TIME_WAIT 狀態(tài)的 TCP 連接,在 macOS 上可以使用如下所示的命令查看活躍的連接:
netstat -tan
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 192.168.50.109.51284 47.95.49.174.443 TIME_WAIT
tcp4 0 0 192.168.50.109.51275 47.95.49.174.443 TIME_WAIT
...
tcp4 0 0 192.168.50.109.51273 203.107.32.116.443 TIME_WAIT
tcp4 0 0 192.168.50.109.51293 203.107.32.116.443 TIME_WAIT
tcp4 0 0 192.168.50.109.51297 203.107.32.116.443 TIME_WAIT
...
當(dāng)我們在主機上通過幾千個并發(fā)來測試服務(wù)器的壓力時,這些用于壓力測試的連接會迅速消耗主機上的 TCP 連接資源,幾乎所有的 TCP 都會處于 TIME_WAIT 狀態(tài)等待銷毀。如果我們真遇到不得不處理單機上的 TIME_WAIT 狀態(tài)的時候,那么可以通過以下幾種方法處理:
- 使用 SO_LINGER 選項并設(shè)置暫存時間 l_linger 為 0,在這時如果我們關(guān)閉 TCP 連接,內(nèi)核就會直接丟棄緩沖區(qū)中的全部數(shù)據(jù)并向服務(wù)端發(fā)送 RST 消息直接終止當(dāng)前的連接7;
- 使用 net.ipv4.tcp_tw_reuse 選項,通過 TCP 的時間戳選項允許內(nèi)核重用處于 TIME_WAIT 狀態(tài)的 TCP 連接8;
- 修改 net.ipv4.ip_local_port_range 選項中的可用端口范圍,增加可同時存在的 TCP 連接數(shù)上限;
- 需要注意的是,另一個常見的 TCP 配置項 net.ipv4.tcp_tw_recycle 已經(jīng)在 Linux 4.12 中移除9,所以我們不能再通過該配置解決 TIME_WAIT 設(shè)計帶來的問題。
TCP 的 TIME_WAIT 狀態(tài)有著非常重要的作用,它是保證 TCP 協(xié)議可靠性不可缺失的設(shè)計,如果能通過加機器解決的話就盡量加機器,如果不能解決的話,我們就需要理解其背后的設(shè)計原理并盡可能避免修改默認(rèn)的配置,就像 Linux 手冊中說的一樣,在修改這些配置時應(yīng)該咨詢技術(shù)專家的建議;在這里,我們再重新回顧一下 TCP 協(xié)議中 TIME_WAIT 狀態(tài)存在的原因,如果客戶端等待的時間不夠長,那么使用相同端口號重新與遠(yuǎn)程建立連接時會造成以下問題:
- 因為數(shù)據(jù)段的網(wǎng)絡(luò)傳輸時間不確定,所以可能會收到上一次 TCP 連接中未被收到的數(shù)據(jù)段;
- 因為客戶端發(fā)出的 ACK 可能還沒有被服務(wù)端接收,服務(wù)端可能還處于 LAST_ACK 狀態(tài),所以它會回復(fù) RST 消息終止新連接的建立;
TIME_WAIT 狀態(tài)是 TCP 與不確定的網(wǎng)絡(luò)延遲斗爭的結(jié)果,而不確定性是 TCP 協(xié)議在保證可靠這條路的最大阻礙。