TIME-WAIT 是 TCP 揮手過程的一個狀態。很多地方都對它有說明,這里只貼兩個圖喚起記憶。下面是 TCP 完整的狀態圖:
來自:http://www.tcpipguide.com/free/t_TCPOperationalOverviewandtheTCPFiniteStateMachineF-2.htm
看到最下面有個 TIME-WAIT 狀態。狀態圖可能看着不那么直觀,可以看這個:
來自:http://www.tcpipguide.com/free/t_TCPConnectionTermination-2.htm
上面實際不一定是只有 Client 才能進入 TIME-WAIT 狀態,而是誰發起 TCP 連接斷開先發的 FIN,誰最終就進入 TIME-WAIT 狀態。
TIME-WAIT 的作用
第一個作用是避免上一個連接延遲到達的數據包被下一個連接錯誤接收。如下圖所示:
來自:https://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux
虛線將兩次連接分開,兩次連接都使用的同一組 TCP Tuple,即 Source IP, Source Port, Destination IP, Destination Port 組合。第一次連接中 SEQ 為 3 的數據包出現了重發,第二次連接中剛好再次使用 SEQ 為 3 這個序號的時候,第一次連接中本來發丟(延遲)的 SEQ 為 3 的數據包在此時到達,就導致這個延遲了的 SEQ 為 3 的數據包被當做正確的數據而接收,之后如果還有 SEQ 為 3 的正常數據包到達會被接收方認為是重復數據包而直接丟棄,導致 TCP 連接接收的數據錯誤。
這種錯誤可能看上去很難發生,因為必須 TCP Tuple 一致,並且 SEQ 號必須 valid 時才會發生,但在高速網絡下發生的可能性會增大(因為 SEQ 號會很快被耗盡而出現折疊重用),所以需要有個 TIME-WAIT 的存在減少上面這種情況的發生,並且 TIME-WAIT 的長度還要長一些, RFC 793 - Transmission Control Protocol 要求 TIME-WAIT 長度要大於兩個 MSL (Maximum segment lifetime - Wikipedia) 。MSL 是人為定下的值,就認為數據包在網絡路由的時間不會超過這么長。等足兩個 MSL 以保證上圖第二次連接建立的時候之前發丟的 SEQ 為 3 的數據包已經在網絡中丟失,不可能再出現在第二次連接中。
另一個作用是被動斷開連接的一方,發出最后一個 FIN 之后進入 LAST ACK 狀態。主動斷開連接的一方在收到 FIN 之后回復 ACK,如果該 ACK 發丟了,被動斷開連接的一方會一直處在 LAST ACK 狀態,並在超時之后重發 FIN。主動斷開連接的一方如果處在 TIME WAIT 狀態,重復收到 FIN 后每次會重發 ACK。但如果 TIME WAIT 時間短,會進入 CLOSED 狀態,此時再收到 FIN 就直接回復 RST 了。這個 RST 會導致被動斷開連接的一方有個錯誤提示,雖然所有數據實際已經成功發到對端。
這里有個疑問,如果連接斷開后 TIME WAIT 時間很短,TIME WAIT 結束之后主動斷開連接一方直接發出 SYN 而被動斷開連接一方還處在 LAST ACK 狀態,因為 SYN 是其不期待的數據會不會觸發其回復 RST 導致主動斷開連接一方 connect 失敗而放棄建立連接?這篇文章 說如果開啟 TCP Timestamp 后處在 LAST ACK 一方會丟棄 SYN 不會回復 RST 而是等到 FIN 超時后重發 FIN,從而讓主動斷開連接一方回復 RST 后再次發起 SYN 最終能保證連接正常建立。但依據是哪里?在下面介紹 tcp_tw_recycle 的地方會對 Linux 內收到數據包的過程做一下梳理,但從目前來看如果連接處在 LAST ACK,收到 SYN 后如果數據包沒有損壞,SEQ 號也符合要求等等各種檢查都能通過,則連接什么都不會做,相當於忽略了這個 SYN,也就是說不管 TCP TImestamp 是否開啟,處在 LAST ACK 時收到 SYN 都不做任何事情。
TIME WAIT 帶來的問題
先引用一個名言:
The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it.
|
據說這話的是 W. Richard Stevens 說的。也就是說 TIME WAIT 可能會給我們帶來一些問題,但我們還是不要把它當成敵人,當成一個異常狀態想方設法的去破壞它的正常工作,而是去利用它,理解它,讓它為我們所用。這里要說 TIME WAIT 的問題只是需要我們去理解它的副作用,不是說 TIME WAIT 真的就很邪惡很討厭。
TIME WAIT 帶來的問題主要是三個:
- 端口占用,導致新的連接可能沒有可用的端口;
- TIME WAIT 狀態的連接依然會占用系統內存;
- 會帶來一些 CPU 開銷
對於第一個問題可以考慮開啟下面說的 tcp_tw_reuse 甚至 tcp_tw_recycle,也能調大 net.ipv4.ip_local_port_range 以獲取更多的可用端口,還可以使用多個 IP 或 Server 開啟多個 port 的方法來避免沒有端口可用的情況。
對於內存上的開銷,進入 TIME WAIT 后應用層實際已經將連接相關信息銷毀了,只是在 kernel 還維護有連接相關信息,所以內存占用只發生在 kernel 內。正常狀態下的 Socket 結構比較復雜,可以看看這里 struct tcp_sock,它里面使用的是struct inet_connection_sock。Linux 為了減少 TIME WAIT 連接的開銷,專門構造了更精簡的 Socket 數據結構給進入 TIME WAIT 狀態的連接用,參看這里 struct tcp_timewait_sock ,它里面用的是inet_timewait_sock。可以看到 TIME WAIT 狀態下連接的結構要比正常連接數據結構簡單不少,在內核的數據結構最多百來字節,即使有 65535 個 TIME WAIT 的連接存在也占不了多少內存,幾十 M 最多了。
對於 CPU 的開銷一般也不大,主要是在建立連接時在 inet_csk_get_port 函數內查找一個可用端口上的開銷。
所以 TIME WAIT 帶來的最主要的副作用就是會占用端口,而端口數量有限,可能導致無法創建新連接的情況。
減少 TIME WAIT 占用端口的方法
SO_LINGER
對於應用層來說調用 send()
后數據並沒有實際寫入網絡,而是先放到一個 buffer 當中,之后慢慢的往網絡上寫。所以會出現應用層想要關閉一個連接時,連接的 buffer 內還有數據沒寫出去,需要等待這部分數據寫出。調用 close()
后等待 buffer 內數據全部寫出去的時間叫做 Linger Time。Linger Time 結束后開始正常的 TCP 揮手過程。
從前面介紹能看到,正常的主動斷開連接一定會進入 TIME WAIT 狀態,但除了正常的連接關閉之外還有非正常的斷開連接的方法,可以不讓連接進入 TIME WAIT 狀態。方法就是給 Socket 配置 SO_LIGNER,這樣在調用 close()
關閉連接的時候主動斷開連接一方不是等待 buffer 內數據發完之后再發送 FIN 而是根據 SO_LINGER 參數配置的超時時間,等到最多這個超時時間這么長后,如果連接 buffer 內還有數據就直接發送 RST 強制重置連接,對方會收到 Connection Reset by peer 的錯誤,同時會導致主動斷開連接一方所有還未來得及發送的數據全部丟棄。如果還未到 SO_LINGER 配置的超時時間連接 buffer 內的數據就全部發完了,就還是發 FIN 走正常揮手邏輯,但這樣主動斷開連接一方還是會進入 TIME WAIT。所以如果主動斷開連接時完全不想讓連接進入 TIME WAIT 狀態,可以直接將 SO_LINGER 設置為 0 ,這樣調用 close()
后會直接發 RST,丟棄 buffer 內所有數據,並讓連接直接進入 CLOSED 狀態。
從上面描述也能看出來其應用場景可能會比較狹窄。看到有地方建議是說收到錯誤數據,或者連接超時的時候通過這種方式直接重置連接,避免有問題的連接還進入 TIME WAIT 狀態。
tcp_tw_reuse、 tcp_tw_recycle 配置和 TCP Timestamp
為了介紹這兩個配置,首先需要介紹一下 TCP Timestamp 機制。
RFC 1323 和 RFC 7323 提出過一個優化,在 TCP option 中帶上兩個 timestamp 值:
TCP Timestamps option (TSopt):
Kind: 8
Length: 10 bytes
+-------+-------+---------------------+---------------------+
|Kind=8 | 10 | TS Value (TSval) |TS Echo Reply (TSecr)|
+-------+-------+---------------------+---------------------+
1 1 4 4
|
TCP 握手時,通信雙方如果都帶有 TCP Timestamp 則表示雙方都支持 TCP Timestamp 機制,之后每個 TCP 包都需要將自己當前機器時間帶在 TSval 中,並且在每次收到對方 TCP 包做 ACK 回復的時候將對方的 TSval 作為 ACK 中 TSecr 字段返回給對方。這樣通信雙方就能在收到 ACK 的時候讀取 TSecr 值並根據當前自己機器時間計算 TCP Round Trip Time,從而根據網絡狀況動態調整 TCP 超時時間,以提高 TCP 性能。請注意這個 option 雖然叫做 Timestamp 但不是真實日期時間,而是一般跟操作系統運行時間相關的一個持續遞增的值。更進一步信息請看 RFC 的鏈接。
除了對 TCP Round Trip 時間做測量外,這個 timestamp 還有個功能就是避免重復收到數據包影響正常的 TCP 連接,這個功能叫做 PAWS,在上面 RFC 中也有介紹。
PAWS (Protection Against Wrapped Sequences)
從 PAWS 的全名上大概能猜想出來它是干什么的。正常來說每個 TCP 包都會有自己唯一的 SEQ,出現 TCP 數據包重傳的時候會復用 SEQ 號,這樣接收方能通過 SEQ 號來判斷數據包的唯一性,也能在重復收到某個數據包的時候判斷數據是不是重傳的。但是 TCP 這個 SEQ 號是有限的,一共 32 bit,SEQ 開始是遞增,溢出之后從 0 開始再次依次遞增。所以當 SEQ 號出現溢出后單純通過 SEQ 號無法標識數據包的唯一性,某個數據包延遲或因重發而延遲時可能導致連接傳遞的數據被破壞,比如:
來自:http://www.sdnlab.com/17530.html
上圖 A 數據包出現了重傳,並在 SEQ 號耗盡再次從 A 遞增時,第一次發的 A 數據包延遲到達了 Server,這種情況下如果沒有別的機制來保證,Server 會認為延遲到達的 A 數據包是正確的而接收,反而是將正常的第三次發的 SEQ 為 A 的數據包丟棄,造成數據傳輸錯誤。PAWS 就是為了避免這個問題而產生的。在開啟 Timestamp 機制情況下,一台機器發的所有 TCP 包的 TSval 都是單調遞增的,PAWS 要求連接雙方維護最近一次收到的數據包的 TSval 值,每收到一個新數據包都會讀取數據包中的 TSval 值跟 Recent TSval 值做比較,如果發現收到的數據包 TSval 沒有遞增,則直接丟棄這個數據包。對於上面圖中的例子有了 PAWS 機制就能做到在收到 Delay 到達的 A 號數據包時,識別出它是個過期的數據包而將其丟掉。tcp_peer_is_proven 是 Linux 一個做 PAWS 檢查的函數。
TCP Timestamp 就像是 SEQ 號的擴展一樣,用以在 SEQ 號相同時候判斷兩個數據包是否相同以及他們的先后關系。TCP Timestamp 時間跟系統運行時間相關,但並不完全對應,也不需要 TCP 通信雙方時間一致。Timestamp 的起跳粒度可以由系統實現決定,粒度不能太粗也不能太細。粒度最粗至少要保證 SEQ 耗盡的時候 Timestamp 跳了一次,從而在 SEQ 號重復的時候能通過 Timestamp 將數據包區分開。Timestamp 跳的粒度越細,能支持的最大發送速度越高。TCP SEQ 是 32 bit,全部耗盡需要發送 2^32 字節的數據(RFC 7323 說是 2^31 字節數據,我還沒弄明白為什么不是 2^32),如果 Timestamp 一分鍾跳一次,那支持的最高發送速度是一分鍾發完 2^32 字節數據;如果 Timestamp 一秒鍾跳一次,那支持的最高發送速度是一秒鍾發完 2^32 字節數據。另外,Timestamp 因為擔負着測量 RTT 的職責,過粗的粒度也會降低探測精度,不能達到效果。
但是 Timestamp 本身也是有限的,一共 32 bit,Timestamp 跳的粒度越細,跳的越快,被耗盡的速度也越快。越短時間被耗盡越會出現和只靠 SEQ 來判斷數據包唯一性相同的問題場景,即收到一個延遲到達的數據包后無法確認它是正常數據包還是延遲數據包。所以一般推薦 Timestamp 是 1ms 或 1s 一跳。假若是 1ms 一跳的話能支持最高 8 Tbps 的傳輸速度,也能在長達 24.8 天才會被耗盡。只要 MSL (Maximum segment lifetime - Wikipedia) 小於 24.8 天,通過 TCP Timestamp 機制就能拒絕同一個連接上 SEQ 相同的重復數據包。MSL 大於 24.8 天幾乎不可能,一個延遲的數據包在 24.8 天后到達接收方,該數據包的 SEQ 、Timestamp 又恰好和一個正常數據包相同,這個概率非常的小。MSL 相關可以參看RFC 793 。
TCP Timestamp 機制開啟之后 PAWS 會自動開啟,控制 TCP Timestamp 的配置為 net.ipv4.tcp_timestamps
一般現在 Linux 系統都是開啟的。因為能提升 TCP 連接性能,付出的代價相對又少。該配置看着叫 ipv4 但對 ipv6 一樣有效。
Linux 上有幾個跟 PAWS 相關的統計信息:
LINUX_MIB_PAWSPASSIVEREJECTED, /* PAWSPassiveRejected */
LINUX_MIB_PAWSACTIVEREJECTED, /* PAWSActiveRejected */
LINUX_MIB_PAWSESTABREJECTED, /* PAWSEstabRejected */
|
PAWS passive rejected 是 tcp_tw_recycle 開啟后,收到 Client 的 SYN 時,因為 SYN 內的 Timestamp 沒有通過 PAWS 檢測而被拒絕的 SYN 數據包數量。這個稍后再說。
PAWS active rejected 是 Client 發出 SYN 后,收到 Server 的 SYN/ACK 但 SYN/ACK 中的 Timestamp 沒有通過 PAWS 檢測而被拒絕的 SYN/ACK 數據包數量。
PAWS established rejected 是正常建立連接后,因為數據包沒有通過 PAWS 檢測而被拒絕的數據包數量。
這三個定義在一起的,在uapi/linux/snmp.h 下,下面會再次介紹這些計數是什么場景下被記錄的。
目前來看 netstat -s 中只展示了 PAWS established rejected 的值,另外兩個沒展示,需要到 /proc/net/netstat
中看:
358306 packets rejects in established connections because of timestamp
|
net.ipv4.tcp_tw_reuse
需要注意的是這里說的 net.ipv4.tcp_tw_reuse 和下面說的 net.ipv4.tcp_tw_recycle 都是雖然名字里有 ipv4 但對 ipv6 同樣生效。
只有開啟了 PAWS 機制之后開啟 net.ipv4.tcp_tw_reuse 才有用,並且僅對 outgoing 的連接有效,即僅對 Client 有效。Linux 中使用到 tcp_tw_reuse 的地方是 tcp_twsk_unique 函數。它是在 __inet_check_established 內被使用,其作用是在 Client 連 Server 的時候如果已經沒有端口可以使用,並且在 Client 端找到個處在 Time Wait 狀態的和 Server 的連接,只要當前時間和該連接最后一次收到數據的時間差值超過 1s,就能立即復用該連接,不用等待連接 Time Wait 結束。
前面說過 Time Wait 有兩個作用,一個是避免同一個 TCP Tuple 上前一個連接的數據包錯誤的被后一個連接接收。因為有了 PAWS 機制,TCP 收到的數據會檢查 TSval 是否大於最近一次收到數據的 TSval,所以這種情況不會發生。舊連接的數據包到達接收方后因為 PAWS 檢測不通過會直接被丟棄,並更新 LINUX_MIB_PAWSESTABREJECTED 計數。
另一個作用是避免主動斷開連接一方最后一個回復的 ACK 丟失而被動斷開連接一方一直處在 LAST ACK 狀態,超時后會再次發 FIN 到主動斷開連接一方。此時如果主動斷開連接一方不在 Time Wait 會觸發主動斷開連接一方發出 RST 讓被動連接一方出現一個 Connection reset by peer 的報錯。不過這個實際上還好,數據至少都發完了。如果被動斷開連接一方還未因超時而重發 FIN 就收到主動斷開連接一方因為 tcp_tw_reuse 提前從 TIME WAIT 狀態退出而發出的 SYN,被動連接一方會立即重發 FIN,主動連接一方收到 FIN 后回復 RST,之后再重發 SYN 開始正常的 TCP 握手。后一個過程圖如下:
來自:https://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux
這篇文章 說沒有開啟 TCP Timestamp 時,被動斷開連接一方處在 LAST_ACK 狀態,收到 SYN 后會回復 RST;開啟了 TCP Timestamp 之后,被動連接一方處在 LAST_ACK 狀態收到 SYN 會丟棄這個 SYN,在 FIN 超時后再次發 FIN, ACK,這里我有些疑惑。不明白為什么 TCP Timestamp 開啟之后處在 LAST ACK 狀態的一方就會默認丟棄對方發來的 SYN。PAWS 只有 Timestamp 不和要求時才會丟消息,但同一台機器上沒有重啟的話 TSval 是逐步遞增的,SEQ 號也是在原來 TIME WAIT 時存下的 SEQ 號基礎上加一個偏移值得到,按說沒有理由會自動丟棄 SYN 的。
還一個要注意到的是 tcp_tw_reuse 在 reuse 連接的時候新創建的連接會復用之前連接保存的最近一次收到數據的 Timestamp。這個是與下面要說的 tcp_tw_recycle 的不同點,也是為什么 tcp_tw_reuse 在使用了 NAT 的網絡下是安全的,而 tcp_tw_recycle 在使用了 NAT 的網絡下是不安全的。因為 tcp_tw_reuse 記錄的最近一次收到數據的 Timestamp 是針對 Per-Connection 的,而 tcp_tw_recycle 記錄的最近一次收到數據的 Timestamp 是 Per-Host 的,在 NAT 存在的情況下同一個 Host 后面有多少機器就說不清了,每台機器時間不同發出數據包的 TSval 也不同,PAWS 可能錯誤的將正常的數據包丟棄所以會導致問題,這個在下面描述 tcp_tw_recycle 的時候再繼續說。
從上面描述來看 tcp_tw_reuse 還是比較安全的,一般是建議開啟。不過該配置對 Server 沒有用,因為只有 outgoing 的連接才會使用。每次 reuse 一個連接后會將 TWRecycled 計數加一,通過 netstat -s 能看到:
7212 time wait sockets recycled by time stamp
|
雖然叫做 TWRecycled 但實際它指的是 reuse 的連接數,不是下面要說的 recycled 的連接數。其反應的是 LINUX_MIB_TIMEWAITRECYCLED 這個計數,在__inet_check_established 內 reuse TIME WAIT 連接后計數
net.ipv4.tcp_tw_recycle
該配置也是要基於 TCP Timestamp,單獨開啟 tcp_tw_recycle 沒有效果。相對 tcp_tw_reuse 來說 tcp_tw_recycle 是個更激進的參數,這個參數在 Linux 的使用參看:
linux/net/ipv4/tcp_input.c - Elixir - Free Electrons
linux/net/ipv4/tcp_minisocks.c - Elixir - Free Electrons
linux/net/ipv4/tcp_ipv4.c - Elixir - Free Electrons
理一下 TCP 收消息過程
為了說清楚 tw_recycle 的使用場景,我准備把接收消息過程理一遍,可能會寫的比較啰嗦,看的時候需要靜下心來慢慢看。
首先鏈路層收到 TCP IPv4 的數據后會走到 tcp_v4_rcv 這里,看到參數是 struct sk_buff,以后有機會記錄一下,NIC 從鏈路接收到數據寫入 Ring Buffer 后就會構造這個 struct sk_buff,並且之后在數據包的處理過程中一直復用同一個 sk_buff,避免內存拷貝。
tcp_v4_rct 首先是進行各種數據包的校驗,根據數據包的 source,dst 信息找到 Socket,是個struct sock 結構。我們這里主要說 TIME WAIT,所以別的東西都先不管。主要是看到下面會調用 tcp_v4_do_rcv。在 tcp_v4_do_rcv 內會開始真的處理 sk_buff 數據內容,如果連接不是 ESTABLSHED,也不是 LISTEN 會走到 tcp_rcv_state_process函數,這個函數是專門處理 TCP 連接各種狀態轉換的。還是那句話,我們關心的是 TIME WAIT,所以別的也都不看,先看連接的狀態轉換。我們知道連接先是在 FIN-WAIT-1,收到對方 ACK 后進入 FIN-WAIT-2,再收到對方 FIN 后進入 TIME-WAIT。在 tcp_rcv_state_process 先找到 FIN-WAIT-1 這個 case,這里先會檢查 acceptable 是否置位,表示收到的 sk_buff 內 ACK flag 是置位的,如果沒有置位會立即返回 1 表示狀態有誤,之后會發 RST。即在 FIN-WAIT-1 狀態下,連接期待的數據必須設置 ACK Flag,沒設置就立即發 RST 重置連接。
如果一切正常,則將連接狀態設置為 FIN-WAIT-2,並讀取 sysctl 的 net.ipv4.tcp_fin_timeout 配置。如果 sk_buff 中沒有同時設置 FIN 說明對方是先回復了 ACK,讓當前連接線進入 FIN-WAIT-2,FIN 在之后的包中發過來。所以此時設置連接狀態並 discard 當前 sk_buff。這里有些疑問,此時連接實際是 Half-Open 的,這里沒有判斷 ACK 內有沒有別的數據就把 sk_buff 丟棄了,從 RFC 793 中似乎沒看到說要求針對 FIN 的 ACK 內必須不能有數據。接着說,看到用 tcp_time_wait 函數將 tw_substate 設置到 FIN-WAIT-2 ,將連接設置為 TIME WAIT,並設置超時時間是 net.ipv4.tcp_fin_timeout 的值。稍后再繼續說 tcp_time_wait 這個函數,還有很多可以挖掘的。
如果 sk_buff 同時設置了 FIN,說明對方是將 FIN 和 ACK 一起發來的,同一個數據包中 FIN 和 ACK 兩個 Flag 都置位,此時並不立即設置 TCP 連接的狀態,而是在稍后在專門處理 FIN 的邏輯中處理 TCP 狀態變換。如果 FIN 和 ACK 一起設置了,不會 discard 數據包,再往下還有個 case,要注意到有個 switch 的 Fall through,也就是說連接不管在 CLOSE_WAIT, CLOSING, FIN_WAIT1, FIN_WAIT2, ESTALISHED 等最終都會進入 tcp_data_queue 來繼續處理這個收到的 sk_buff。在 tcp_data_queue 中我們看到帶着 FIN 的 sk_buff 會交給專門的 tcp_fin 來處理。因為在 tcp_rcv_state_process 內我們剛剛將連接狀態設置為 TCP_FIN_WAIT2 所以在 tcp_fin 的 switch 內我們找到 TCP_FIN_WAIT2 的處理邏輯,即回復 ACK 並通過 tcp_time_wait 函數設置 tw_substate 為 TCP_TIME_WAIT ,將連接設置為 TIME WAIT 狀態,並設置超時時間是 0。
接下來我們看看 tcp_time_wait 函數 ,這個函數完成了將連接轉換到 TIME WAIT 狀態的邏輯。如果 tw_recycle 、TCP Timestamp 開啟,會先 Per-Host 的緩存連接最后一次收到數據的對方 TSval。將連接從普通的 struct sock 轉換為 TIME WAIT 狀態下連接特有的更加精簡的 struct tcp_timewait_sock 結構。並 設置連接處在 TIME WAIT 的超時時間,能看到 tw_recycle 開啟的話 tw_timeout 只有一個 RTO 這么長,能大幅度減少連接處在 TIME WAIT 的時間。而沒有開啟 tw_recycle 的話超時時間是 TIME_WAIT_LEN,該值是個不可配置的 macro。TIME WAIT 超時后會執行 tw_timer_handler將連接清理。tw_timer_handler 是在構造 inet_timewait_sock 執行 inet_twsk_alloc 將一個 TIME WAIT 的 socket 和 tw_timer_handler 關聯起來的。
正常的連接使用的是 struct tcp_sock 內部第一個結構是 struct sock,TIME WAIT 的連接使用的是 struct tcp_timewait_sock 內部第一個結構是 struct inet_timewait_sock。struct sock 和 struct inet_timewait_sock 內部第一個結構都是 struct sock_common。struct sock_common 是連接相關最核心的信息,標識一個連接必須要有這個。而為了減少 TIME WAIT 連接對內存空間的占用,所以弄了精簡的 struct tcp_timewait_sock 可以看到它相對 strcut tcp_sock 內容要少的多,並且內部 struct inet_timewait_sock 相對於 struct sock 來說內容也少了很多,整個結構很精簡。看到 struct sock 和 struct inet_time_wait_sock 第一個結構都是 struct sock_common 所以如果是訪問 struct sock_common 的內容,指向這兩個 struct 的指針是能夠相互轉換的。這兩個 struct 內部定義了很多宏,用於方便的訪問 struct sock_common 的內容。比如 struct sock 內的 sk_state 和 struct inet_timewait_sock 內的 tw_state 實際都訪問的是 struct sock_common 的 skc_common。
說 struct socket 等結構主要是為了說明 tcp_time_wait 內是如何將 socket 狀態設置為 TIME WAIT的。一般設置 socket 狀態使用的是 tcp_set_state 這個函數,比如在 tcp_rcv_state_process 內之前看到的。但 TIME WAIT 這個狀態卻不是 tcp_set_state 來設置的,而是在從 struct socket 構造 struct inet_timewait_sock 時設置的,構造 struct inet_timewait_sock 會默認設置 tw_state 為 TCP_TIME_WAIT。 從前面描述來看,連接在 FIN-WAIT-1 狀態時收到 ACK 且 FIN 置位時,會在回復 ACK 后執行 tcp_time_wait,此時連接確實應該進入 TIME WAIT 狀態;但在收到 ACK 且 FIN 沒有置位的時候,連接實際處在 FIN-WAIT-2 狀態卻也會執行 tcp_time_wait。tcp_time_wait 內會將連接狀態默認的設置為 TCP_TIME_WAIT,這沒有實際反映出當前連接的實際狀態,所以 struct inet_timewait_sock 內還有個 tw_substate 用以記錄這個連接的實際狀態。如果連接實際處在 FIN-WAIT-2,收到對方 FIN 后在 tcp_v4_rcv 內根據 sk_buff 找到 Socket,此時的 Socket 雖然使用的是 struct sock 指針,但實際指的是個 struct inet_timewait_sock,訪問其 sk_state 實際訪問的是 struct sock_common 的 sk_state 字段,也即 struct inet_timewait_sock 的 tw_state 字段。所以讀到的當前狀態是 TCP_TIME_WAIT。於是進入 tcp_timewait_state_process 函數處理數據包。當連接的 sk_state 處在 TCP_TIME_WAIT 時,所有收到的數據包均交給 tcp_timewait_state_process 處理。在 tcp_timewait_state_process 內可以看到會檢查 tw_substate 是不是 TCP_FIN_WAIT2,是的話會將 tw_substate 也設置為 TCP_TIME_WAIT,並會重新設置 TIME WAIT 的 timeout 時間。設置的邏輯跟連接在 FIN-WAIT-1 下收到 ACK 且 FIN 置位時一樣,會判斷 tw_recycle 是否開啟,開啟的話 timeout 就是一個 RTO,不是的話就是 TIME_WAIT_LEN。
從上面這么一大段的描述中我們得到這么一些信息:
- 連接在進入 FIN-WAIT-2 后內核維護的 socket 就會改為和 TIME WAIT 狀態時一樣的精簡結構,以減少內存占用
- 連接進入 FIN-WAIT-2 后為了避免對方不給回復 FIN,所以會設置
net.ipv4.tcp_fin_timeout
這么長的超時時間,超時后會按照清理 TIME WAIT 連接的邏輯清理 FIN-WAIT-2 連接 - tcp_tw_recycle 開啟后,timeout 只有一個 RTO 這個正常來說是會大大低於 TCP_TIMEWAIT_LEN 的。 這里就說明 tcp_tw_recycle 是對 outgoing 和 incoming 的連接都會產生效果,不管連接是誰先發起創建的,只要是開啟 tcp_tw_recycle 的機器先斷開連接,其就會進入 TIME WAIT 狀態(或 FIN-WAIT-2),並且會受到 tcp_tw_recycle 的影響,大幅度縮短 TIME WAIT 的時間。
tcp_tw_recycle 為什么是不安全的
跟 tcp_tw_reuse 一樣,由於 PAWS 機制的存在,縮短 TIME WAIT 后同一個 TCP Tuple 上前一個連接的數據包不會被后一個連接錯誤的接收。TIME WAIT 另一個要處理的 LAST ACK 的問題跟 tcp_tw_reuse 也一樣,不會產生很大的問題,新的連接依然能正常建立,舊連接的數據也能保證都發到對方,只是舊連接上可能會產生一個 Connection reset by peer 的錯誤。那 tcp_tw_recycle 是不是就是安全的呢?為什么那么多人都不推薦開啟這個機制呢?
原因是前面說過 tcp_tw_reuse 新建立的連接會復用前一次連接保存的 Recent TSval 即最后一次收到數據的 Timestamp 值,這個值是 Per-Connection 的,即使有 NAT 的存在,也不會產生問題。比如當前機器是 A 要和 B C D 三台機器建立連接,假設 B C D 三台機器都在 NAT 之后,對 A 來說 B C D 使用的是相同的 IP 或者稱為 Host。B C D 三台機器因為啟動時間不同,A 與他們建立連接之后他們發來的 TSval 都不相同,有前有后。因為 A 是以 Per-Connection 的保存 TSval ,不會出現比如因為 C 機器時間比 B 晚,A 收到 B 的一條消息之后再收到 C 的消息而丟棄 C 的數據的情況。因為在 A 上為 B C D 三台機器分別保存了 Recent TSval,他們之間不會混淆。
但是對於 tcp_tw_recycle 來說,TIME WAIT 之后連接信息快速的被回收,Per-Connection 保存的 TSval 記錄就被清除了,取而代之的是另一個 Per-Host 的 TSval cache,在這里能看到在 tcp_time_wait當 tcp_tw_recycle 和 TCP Timestamp 都開啟后,連接進入 TIME WAIT 之前會將 socket 的 Timestamp 存下來,並且存儲方法是 Per-Host 的。這樣在 NAT 存在的場景下就有問題了,還是上面的例子,B C D 在同一個 NAT 之后,具有相同的 Host,B 先跟 A 建立連接,此時 Per-Host cache 更新為 B 的機器時間,之后 C 來跟 A 建立連接(接收 incoming 連接請求相關邏輯可以看tcp_conn_request 這個函數),讀取 Per-Host Cache 后發現 C 的 SYN 中 TSval 比 Cache 的 TSval 時間要早,於是直接默默丟棄 C 的 SYN。這種情況會更新 LINUX_MIB_PAWSPASSIVEREJECTED 從而在 /proc/net/netstat
下的 PAWSPassive 看這個計數。在網上看到好些地方說到這個問題的時候都說會更新 PAWSEstab 計數,這是不對的。
除了上面場景中 A 會丟棄 C 的 SYN 之外,還有別的引起問題的場景。比如 A 跟 B 建立了連接,Per-Host TSval 更新為 B 的時間,之后又要去跟 C 建立連接( IPv4 下創建 outgoing 連接相關邏輯可以看 tcp_v4_connect 函數),A 發出 SYN 時會更新該連接的 Recent TSval 為緩存的 Per-Host TSval 時間,即 B 的時間。假設 A 這一側網絡環境比較簡單,IP 只有 A 一台機器在用,於是 C 校驗 A 的 SYN 通過,所以 C 會正常回復 SYN//ACK 給 A,但 SYN//ACK 到達 A 之后,因為 SYN//ACK 帶着 C 的 TSval,其時間晚於 B 的時間,導致 A 直接丟棄 C 發來的 SYN/ACK,並更新 LINUX_MIB_PAWSACTIVEREJECTED 計數,該計數能在 /proc/net/netstat
下的 PAWSActive 看到。
所以,tcp_tw_recycle 是不推薦開啟的,因為 NAT 在網絡上大量的存在,配合 tcp_tw_recycle 會出現問題。
如果有連接由於 tcp_tw_recycle 開啟而被清理的話,會更新 /proc/net/netstat
的 TWKilled 計數,在 Linux 內是 LINUX_MIB_TIMEWAITKILLED,請注意和 TCPRecycled 區分。這個計數在 tw_timer_handler 中當 tw->tw_kill 有值的時候會更新,而 tw_kill 是在連接進入 TIME WAIT,schedule 清理連接任務時候被設置的,在 __inet_twsk_schedule。
跟 TIME WAIT 相關的還有一個計數叫做 TW,在 Linux 內部叫做 LINUX_MIB_TIMEWAITED。它也是在 tw_timer_handler 中被更新,可以看到只要不是 tw_kill 都會當做普通的 TW 被計數,我理解只要是正常 TIME WAIT 結束的都會計入這里,被 recycle 的會計入 TWKilled,被 reuse 的會計入 TWRecycled。但 netstat -s 對應 TW 的描述是:
69598943 TCP sockets finished time wait in fast timer
|
不明白這里 fast timer 是什么意思。
net.ipv4.tcp_max_tw_buckets 是什么
用於限制系統內處在 TIME WAIT 的最大連接數量,當 TIME WAIT 的連接超過限制之后連接會直接被關閉進入 CLOSED 狀態,並會打印日志:
TCP time wait bucket table overflow
|
可以看出這個限制有些暴力,得盡量去避免。還是之前的話,TIME WAIT 是有自己的作用的,暴力干掉它對我們並沒有好處。
tcp_max_tw_buckets 這個限制主要是控制 TIME WAIT 連接占用的資源數,包括內存、CPU 和端口資源。避免 denial-of-service 攻擊。比如端口全部被占用處在 TIME WAIT 狀態,可能會出現沒有多余的端口來建立新的連接。
SO_REUSEADDR 是什么, SO_REUSEPORT 是什么
看完上面內容之后可能會和我一樣產生這個疑問,因為這兩個從名字上看上去跟 tcp_tw_reuse 很像。
關於 SO_REUSEADDR 和 SO_REUSEPORT 這篇文章介紹的特別好,強烈建議一看。唯一是里面關於 TIME WAIT 內容個人認為是不正確的,想先說明一下,避免以后自己再看這個文章的時候搞混淆。他說:
That's why a socket that still has data to send will go into a state called TIME_WAIT when you close it. In that state it will wait until all pending data has been successfully sent or until a timeout is hit, in which case the socket is closed forcefully.
|
實際上連接進入 TIME WAIT 之后是不可能再有數據發出的,因為能進入 TIME WAIT 一定是已經收到了對方的 FIN,此時對方期待的只有 ACK,發應用層數據會引起對方回復 RST。
還有這里:
The amount of time the kernel will wait before it closes the socket, regardless if it still has pending send data or not, is called the Linger Time. The Linger Time is globally configurable on most systems and by default rather long (two minutes is a common value you will find on many systems). It is also configurable per socket using the socket option SO_LINGER which can be used to make the timeout shorter or longer, and even to disable it completely.
|
關於 Linger Time 的描述跟 TIME WAIT 混淆了,Linger Time 並不是全局配置的,最長也不是 2 分鍾,這個都是 TIME WAIT 的長度。Linger TIme 確實也有默認長度,但是個非常大的值,參看這里。所以基本能認為不設置 SO_LINGER 的話,close()
調用后會一直等到 buffer 內數據發完才會開始斷開連接流程。
但是瑕不掩瑜,這篇文章把 SO_REUSEADDR 和 SO_REUSEPORT 講的很清楚。
上面介紹過的 tcp_tw_reuse 和 tcp_tw_recycle 都是內核級參數,使用之后會在整個系統產生作用,所有創建的連接都受到影響。SO_REUSEADDR 和 SO_REUSEPORT 都是單個連接級的參數,使用后只能對單個連接產生影響,不是整個系統級別的。
SO_REUSEADDR
有兩個作用:
一個是 bind()
socket 時可以綁定 “any address” IPv4 下是 0.0.0.0
或在 IPv6 下是::
。SO_REUSEADDR 不開啟的話,這個 any address 會和機器具體使用的 IP 沖突,如果綁定的端口一致會報錯。比如本地有兩個網卡,IP 分別是 192.168.0.1 和 10.0.0.1。如果不開啟 SO_REUSEADDR,綁定 0.0.0.0 的某個端口比如 21 之后,再想綁定某個具體的 IP 192.168.0.1 的 21 端口就不允許了。而開啟 SO_REUSEADDR 之后除非是 IP 和 Port 都被綁定過才會報錯。有個表:
來自: https://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t
上表中全部默認使用 BSD 系統,並且是 socketA 先綁定,之后再綁定 socketB。ON/OFF表示 SO_REUSEADDR 是否開啟不會影響結果。
另一個作用是當連接主動斷開后進入 TIME WAIT 狀態,不開啟 SO_REUSEADDR 的話,TIME WAIT 狀態下連接的 IP 和 Port 也是被占用的,同一個 IP 和 Port 不能再次被 bind。但是開啟 SO_REUSEADDR ,連接進入 TIME WAIT 后它使用的 IP 和 Port 能再次被應用 bind。bind 時會忽略同一個 IP 和 Port 的連接是否在 TIME WAIT 狀態。
需要說明的是上面 SO_REUSEADDR 的行為是 BSD 系統上的,在 Linux 上會有所不同。在 Linux 上上圖第六行的綁定是不行的,即先綁定 any address 再綁定 specific address 並且端口相同會被拒絕,反過來也一樣,比如先綁定 192.168.1.0:21 再綁定 0.0.0.0:21 是被拒絕的。也就是說在 Linux 上上述 SO_REUSEADDR 第一個作用是沒有用的,因為不即使不設置 SO_REUSEADDR 綁定兩個不同的 IP 也是允許的。SO_REUSEADDR 在 BSD 上的第二個作用和在 Linux 上相同。除此之外,Linux 上的 SO_REUSEADDR 還有第三個作用,設置后允許同一個 specific addr 和 port 被多個 socket 綁定,行為和下面要說的 SO_REUSEPORT 類似。主要是因為 Linux 3.9 之前沒有 SO_REUSEPORT,但又有 SO_REUSEPORT 的使用場景,於是 SO_REUSEADDR 發展出了 SO_REUSEPORT 的能力來替代 SO_REUSEPORT,但 Linux 3.9 之后有了專門的參數 SO_REUSEPORT,SO_REUSEADDR 則保持原狀。
SO_REUSEPORT
BSD 系統上設置后允許同一個 specific address, port 被多個 Socket 綁定,只要這些 Socket 綁定地址的時候都設置了 SO_REUSEPORT。聽上去這個 SO_REUSEPORT 干的更像是 reuse addr 的活。如果占用 source addr 和 port 的連接處在 TIME WAIT 狀態,並且沒有設置 SO_REUSEPORT 那該地址和端口不能被另一個 socket 綁定。事實上 SO_REUSEPORT 和 TIME WAIT 沒有什么關系,設置后能不能綁定某個端口和地址完全是看這個端口和地址現在有沒有被別的連接使用,如果有則要看這個連接是否開啟了 SO_REUSEPORT,跟這個連接被占用時處在 TCP 的什么狀態完全無關。
在 Linux 上相對 BSD 還要求地址和端口重用必須是同一個 user 之間,如果地址和端口被某個 Socket 占用,並且這個 Socket 是另一個 user 的,那即使該 Socket 綁定時開啟了 SO_REUSEPORT 也不能再次被綁定。另外 Linux 還會做一些 load balancing 的工作,對 UDP 來說一個連接的數據包會被均勻分發到所有綁定同一個 addr 和 port 的 socket 上,對於 TCP 來說是 accept 會被均勻的分發到綁定在同一個 addr 和 port 的連接上。
tcp_fin_timeout
很多地方寫的說 net.ipv4.tcp_fin_timeout
將這個配置減小一些能縮短 TIME WAIT 時間,但是我們從介紹 tcp_tw_recycle 那節看到,如果沒設置 tcp_tw_recycle 的話 TIME WAIT 時間是個固定值 TCP_TIMEWAIT_LEN,這個值是個 macro :
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
* state, about 60 seconds */
|
也就是說它的長度並不是個可配置項。
net.ipv4.tcp_fin_timeout
的定義在 struct tcp_prot 中 其默認值定義為:
#define TCP_FIN_TIMEOUT TCP_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.
*/
|
也就是說這個配置實際是去控制 FIN-WAIT-2 時間的,只是默認值恰好跟 TIME_WAIT 一致。就不貼使用這個配置的地方了,總之該配置是控制 FIN-WAIT-2 的,也就是說主動斷開連接一方發出 FIN 也收到 ACK 后等待對方發 FIN 的時間,該配置並不會影響到 TIME WAIT 長度。
參考文獻
- 有很多參考了這篇文章:Coping with the TCP TIME-WAIT state on busy Linux servers | Vincent Bernat
- TCP Timestamp 、PAWS 相關主要參考了 RFC 1323(已被 RFC 7323 取代)、RFC 7323、RFC 6191
- 這個雖然和 1 很多內容重復,但能用來相互印證,至少在未開啟 TCP Timestamp 機制時被斷開連接一方處在 LAST ACK 狀態又沒收到 ACK 的處理方式說的不一樣
- linux - Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ? Do they mean the same across all major operating systems? - Stack Overflow
- https://stackoverflow.com/questions/410616/increasing-the-maximum-number-of-tcp-ip-connections-in-linux
- linux - Dropping of connections with tcp_tw_recycle - Stack Overflow
- Linux服務器丟包故障的解決思路及引申的TCP/IP協議棧理論 | SDNLAB | 專注網絡創新技術
- Improve Linux tcp_tw_recycle man page entry - Troy Davis, Seattle
- https://idea.popcount.org/2014-04-03-bind-before-connect/