被微信面麻了,問的太細節(jié)了。。。
大家好,我是小林。
上周有個讀者在面試微信的時候,被問到既然打開 net.ipv4.tcp_tw_reuse 參數(shù)可以快速復(fù)用處于 TIME_WAIT 狀態(tài)的 TCP 連接,那為什么 Linux 默認是關(guān)閉狀態(tài)呢?
好家伙,真的問好細節(jié)!
當(dāng)時看到讀者這個問題的時候,我也是一臉懵逼的,經(jīng)過我的一番思考后,終于知道怎么回答這題了。
其實這題在變相問「如果 TIME_WAIT 狀態(tài)持續(xù)時間過短或者沒有,會有什么問題?」
因為開啟 tcp_tw_reuse 參數(shù)可以快速復(fù)用處于 TIME_WAIT 狀態(tài)的 TCP 連接時,相當(dāng)于縮短了 TIME_WAIT 狀態(tài)的持續(xù)時間。
可能有的同學(xué)會問說,使用 tcp_tw_reuse 快速復(fù)用處于 TIME_WAIT 狀態(tài)的 TCP 連接時,是需要保證 net.ipv4.tcp_timestamps 參數(shù)是開啟的(默認是開啟的),而 tcp_timestamps 參數(shù)可以避免舊連接的延遲報文,這不是解決了沒有 TIME_WAIT 狀態(tài)時的問題了嗎?
是解決部分問題,但是不能完全解決,接下來,我跟大家聊聊這個問題。
什么是 TIME_WAIT 狀態(tài)?
TCP 四次揮手過程,如下圖:
- 客戶端打算關(guān)閉連接,此時會發(fā)送一個 TCP 首部 FIN 標(biāo)志位被置為 1 的報文,也即 FIN 報文,之后客戶端進入 FIN_WAIT_1 狀態(tài)。
- 服務(wù)端收到該報文后,就向客戶端發(fā)送 ACK 應(yīng)答報文,接著服務(wù)端進入 CLOSED_WAIT 狀態(tài)。
- 客戶端收到服務(wù)端的 ACK 應(yīng)答報文后,之后進入 FIN_WAIT_2 狀態(tài)。
- 等待服務(wù)端處理完數(shù)據(jù)后,也向客戶端發(fā)送 FIN 報文,之后服務(wù)端進入 LAST_ACK 狀態(tài)。
- 客戶端收到服務(wù)端的 FIN 報文后,回一個 ACK 應(yīng)答報文,之后進入 TIME_WAIT 狀態(tài)
- 服務(wù)器收到了 ACK 應(yīng)答報文后,就進入了 CLOSE 狀態(tài),至此服務(wù)端已經(jīng)完成連接的關(guān)閉。
- 客戶端在經(jīng)過 2MSL 一段時間后,自動進入 CLOSE 狀態(tài),至此客戶端也完成連接的關(guān)閉。
你可以看到,兩個方向都需要一個 FIN 和一個 ACK,因此通常被稱為四次揮手。
這里一點需要注意是:主動關(guān)閉連接的,才有 TIME_WAIT 狀態(tài)。
可以看到,TIME_WAIT 是「主動關(guān)閉方」斷開連接時的最后一個狀態(tài),該狀態(tài)會持續(xù) 2MSL(Maximum Segment Lifetime) 時長,之后進入CLOSED 狀態(tài)。
MSL 指的是 TCP 協(xié)議中任何報文在網(wǎng)絡(luò)上最大的生存時間,任何超過這個時間的數(shù)據(jù)都將被丟棄。雖然 RFC 793 規(guī)定 MSL 為 2 分鐘,但是在實際實現(xiàn)的時候會有所不同,比如 Linux 默認為 30 秒,那么 2MSL 就是 60 秒。
MSL 是由網(wǎng)絡(luò)層的 IP 包中的 TTL 來保證的,TTL 是 IP 頭部的一個字段,用于設(shè)置一個數(shù)據(jù)報可經(jīng)過的路由器的數(shù)量上限。報文每經(jīng)過一次路由器的轉(zhuǎn)發(fā),IP 頭部的 TTL 字段就會減 1,減到 0 時報文就被丟棄。
MSL 與 TTL 的區(qū)別:MSL 的單位是時間,而 TTL 是經(jīng)過路由跳數(shù)。所以 MSL 應(yīng)該要大于等于 TTL 消耗為 0 的時間,以確保報文已被自然消亡。
TTL 的值一般是 64,Linux 將 MSL 設(shè)置為 30 秒,意味著 Linux 認為數(shù)據(jù)報文經(jīng)過 64 個路由器的時間不會超過 30 秒,如果超過了,就認為報文已經(jīng)消失在網(wǎng)絡(luò)中了。
為什么要設(shè)計 TIME_WAIT 狀態(tài)?
設(shè)計 TIME_WAIT 狀態(tài),主要有兩個原因:
- 防止歷史連接中的數(shù)據(jù),被后面相同四元組的連接錯誤的接收;
- 保證「被動關(guān)閉連接」的一方,能被正確的關(guān)閉;
原因一:防止歷史連接中的數(shù)據(jù),被后面相同四元組的連接錯誤的接收
為了能更好的理解這個原因,我們先來了解序列號(SEQ)和初始序列號(ISN)。
- 序列號,是 TCP 一個頭部字段,標(biāo)識了 TCP 發(fā)送端到 TCP 接收端的數(shù)據(jù)流的一個字節(jié),因為 TCP 是面向字節(jié)流的可靠協(xié)議,為了保證消息的順序性和可靠性,TCP 為每個傳輸方向上的每個字節(jié)都賦予了一個編號,以便于傳輸成功后確認、丟失后重傳以及在接收端保證不會亂序。序列號是一個 32 位的無符號數(shù),因此在到達 4G 之后再循環(huán)回到 0。
- 初始序列號,在 TCP 建立連接的時候,客戶端和服務(wù)端都會各自生成一個初始序列號,它是基于時鐘生成的一個隨機數(shù),來保證每個連接都擁有不同的初始序列號。初始化序列號可被視為一個 32 位的計數(shù)器,該計數(shù)器的數(shù)值每 4 微秒加 1,循環(huán)一次需要 4.55 小時。
給大家抓了一個包,下圖中的 Seq 就是序列號,其中紅色框住的分別是客戶端和服務(wù)端各自生成的初始序列號。
通過前面我們知道,序列號和初始化序列號并不是無限遞增的,會發(fā)生回繞為初始值的情況,這意味著無法根據(jù)序列號來判斷新老數(shù)據(jù)。
假設(shè) TIME-WAIT 沒有等待時間或時間過短,被延遲的數(shù)據(jù)包抵達后會發(fā)生什么呢?
- 服務(wù)端在關(guān)閉連接之前發(fā)送的 SEQ = 301 報文,被網(wǎng)絡(luò)延遲了。
- 接著,服務(wù)端以相同的四元組重新打開了新連接,前面被延遲的 SEQ = 301 這時抵達了客戶端,而且該數(shù)據(jù)報文的序列號剛好在客戶端接收窗口內(nèi),因此客戶端會正常接收這個數(shù)據(jù)報文,但是這個數(shù)據(jù)報文是上一個連接殘留下來的,這樣就產(chǎn)生數(shù)據(jù)錯亂等嚴重的問題。
為了防止歷史連接中的數(shù)據(jù),被后面相同四元組的連接錯誤的接收,因此 TCP 設(shè)計了 TIME_WAIT 狀態(tài),狀態(tài)會持續(xù) 2MSL 時長,這個時間足以讓兩個方向上的數(shù)據(jù)包都被丟棄,使得原來連接的數(shù)據(jù)包在網(wǎng)絡(luò)中都自然消失,再出現(xiàn)的數(shù)據(jù)包一定都是新建立連接所產(chǎn)生的。
原因二:保證「被動關(guān)閉連接」的一方,能被正確的關(guān)閉
如果客戶端(主動關(guān)閉方)最后一次 ACK 報文(第四次揮手)在網(wǎng)絡(luò)中丟失了,那么按照 TCP 可靠性原則,服務(wù)端(被動關(guān)閉方)會重發(fā) FIN 報文。
假設(shè)客戶端沒有 TIME_WAIT 狀態(tài),而是在發(fā)完最后一次回 ACK 報文就直接進入 CLOSED 狀態(tài),如果該 ACK 報文丟失了,服務(wù)端則重傳的 FIN 報文,而這時客戶端已經(jīng)進入到關(guān)閉狀態(tài)了,在收到服務(wù)端重傳的 FIN 報文后,就會回 RST 報文。
服務(wù)端收到這個 RST 并將其解釋為一個錯誤(Connection reset by peer),這對于一個可靠的協(xié)議來說不是一個優(yōu)雅的終止方式。
為了防止這種情況出現(xiàn),客戶端必須等待足夠長的時間確保對端收到 ACK,如果對端沒有收到 ACK,那么就會觸發(fā) TCP 重傳機制,服務(wù)端會重新發(fā)送一個 FIN,這樣一去一來剛好兩個 MSL 的時間。
但是你可能會說重新發(fā)送的 ACK 還是有可能丟失啊,沒錯,但 TCP 已經(jīng)等待了那么長的時間了,已經(jīng)算仁至義盡了。
tcp_tw_reuse 是什么?
在 Linux 操作系統(tǒng)下,TIME_WAIT 狀態(tài)的持續(xù)時間是 60 秒,這意味著這 60 秒內(nèi),客戶端一直會占用著這個端口。要知道,端口資源也是有限的,一般可以開啟的端口為 32768~61000 ,也可以通過如下參數(shù)設(shè)置指定范圍:
- net.ipv4.ip_local_port_range
那么,如果如果主動關(guān)閉連接方的 TIME_WAIT 狀態(tài)過多,占滿了所有端口資源,則會導(dǎo)致無法創(chuàng)建新連接。
不過,Linux 操作系統(tǒng)提供了兩個可以系統(tǒng)參數(shù)來快速回收處于 TIME_WAIT 狀態(tài)的連接,這兩個參數(shù)都是默認關(guān)閉的:
- net.ipv4.tcp_tw_reuse,如果開啟該選項的話,客戶端(連接發(fā)起方) 在調(diào)用 connect() 函數(shù)時,內(nèi)核會隨機找一個 TIME_WAIT 狀態(tài)超過 1 秒的連接給新的連接復(fù)用,所以該選項只適用于連接發(fā)起方。
- net.ipv4.tcp_tw_recycle,如果開啟該選項的話,允許處于 TIME_WAIT 狀態(tài)的連接被快速回收,該參數(shù)在 NAT 的網(wǎng)絡(luò)下是不安全的!詳細見這篇文章介紹:字節(jié)面試:SYN 報文什么時候情況下會被丟棄?
要使得上面這兩個參數(shù)生效,有一個前提條件,就是要打開 TCP 時間戳,即 net.ipv4.tcp_timestamps=1(默認即為 1)。
開啟了 tcp_timestamps 參數(shù),TCP 頭部就會使用時間戳選項,它有兩個好處,一個是便于精確計算 RTT ,另一個是能防止序列號回繞(PAWS),我們先來介紹這個功能。
序列號是一個 32 位的無符號整型,上限值是 4GB,超過 4GB 后就需要將序列號回繞進行重用。這在以前網(wǎng)速慢的年代不會造成什么問題,但在一個速度足夠快的網(wǎng)絡(luò)中傳輸大量數(shù)據(jù)時,序列號的回繞時間就會變短。如果序列號回繞的時間極短,我們就會再次面臨之前延遲的報文抵達后序列號依然有效的問題。
為了解決這個問題,就需要有 TCP 時間戳。
試看下面的示例,假設(shè) TCP 的發(fā)送窗口是 1 GB,并且使用了時間戳選項,發(fā)送方會為每個 TCP 報文分配時間戳數(shù)值,我們假設(shè)每個報文時間加 1,然后使用這個連接傳輸一個 6GB 大小的數(shù)據(jù)流。
32 位的序列號在時刻 D 和 E 之間回繞。假設(shè)在時刻B有一個報文丟失并被重傳,又假設(shè)這個報文段在網(wǎng)絡(luò)上繞了遠路并在時刻 F 重新出現(xiàn)。如果 TCP 無法識別這個繞回的報文,那么數(shù)據(jù)完整性就會遭到破壞。
使用時間戳選項能夠有效的防止上述問題,如果丟失的報文會在時刻 F 重新出現(xiàn),由于它的時間戳為 2,小于最近的有效時間戳(5 或 6),因此防回繞序列號算法(PAWS)會將其丟棄。
防回繞序列號算法要求連接雙方維護最近一次收到的數(shù)據(jù)包的時間戳(Recent TSval),每收到一個新數(shù)據(jù)包都會讀取數(shù)據(jù)包中的時間戳值跟 Recent TSval 值做比較,如果發(fā)現(xiàn)收到的數(shù)據(jù)包中時間戳不是遞增的,則表示該數(shù)據(jù)包是過期的,就會直接丟棄這個數(shù)據(jù)包。
為什么 tcp_tw_reuse 默認是關(guān)閉的?
通過前面這么多鋪墊,終于可以說這個問題了。
開啟 tcp_tw_reuse 會有什么風(fēng)險呢?我覺得會有 2 個問題。
第一個問題
我們知道開啟 tcp_tw_reuse 的同時,也需要開啟 tcp_timestamps,意味著可以用時間戳的方式有效的判斷回繞序列號的歷史報文。
但是在看我看了防回繞序列號算法的源碼后,發(fā)現(xiàn)對于 RST 報文的時間戳即使過期了,只要 RST 報文的序列號在對方的接收窗口內(nèi),也是能被接受的。
下面 tcp_validate_incoming 函數(shù)就是驗證接收到的 TCP 報文是否合格的函數(shù),其中第一步就會進行 PAWS 檢查,由 tcp_paws_discard 函數(shù)負責(zé)。
- static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th, int syn_inerr)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- /* RFC1323: H1. Apply PAWS check first. */
- if (tcp_fast_parse_options(sock_net(sk), skb, th, tp) &&
- tp->rx_opt.saw_tstamp &&
- tcp_paws_discard(sk, skb)) {
- if (!th->rst) {
- ....
- goto discard;
- }
- /* Reset is accepted even if it did not pass PAWS. */
- }
當(dāng) tcp_paws_discard 返回 true,就代表報文是一個歷史報文,于是就要丟棄這個報文。但是在丟掉這個報文的時候,會先判斷是不是 RST 報文,如果不是 RST 報文,才會將報文丟掉。也就是說,即使 RST 報文是一個歷史報文,并不會被丟棄。
假設(shè)有這樣的場景,如下圖:
- 客戶端向一個還沒有被服務(wù)端監(jiān)聽的端口發(fā)起了 HTTP 請求,接著服務(wù)端就會回 RST 報文給對方,很可惜的是 RST 報文被網(wǎng)絡(luò)阻塞了。
- 由于客戶端遲遲沒有收到 TCP 第二次握手,于是重發(fā)了 SYN 包,與此同時服務(wù)端已經(jīng)開啟了服務(wù),監(jiān)聽了對應(yīng)的端口。于是接下來,客戶端和服務(wù)端就進行了 TCP 三次握手、數(shù)據(jù)傳輸(HTTP應(yīng)答-響應(yīng))、四次揮手。
- 因為客戶端開啟了 tcp_tw_reuse,于是快速復(fù)用 TIME_WAIT 狀態(tài)的端口,又與服務(wù)端建立了一個與剛才相同的四元組的連接。
- 接著,前面被網(wǎng)絡(luò)延遲 RST 報文這時抵達了客戶端,而且 RST 報文的序列號在客戶端的接收窗口內(nèi),由于防回繞序列號算法不會防止過期的 RST,所以 RST 報文會被客戶端接受了,于是客戶端的連接就斷開了。
上面這個場景就是開啟 tcp_tw_reuse 風(fēng)險,因為快速復(fù)用 TIME_WAIT 狀態(tài)的端口,導(dǎo)致新連接可能被回繞序列號的 RST 報文斷開了,而如果不跳過 TIME_WAIT 狀態(tài),而是停留 2MSL 時長,那么這個 RST 報文就不會出現(xiàn)下一個新的連接。
可能大家會有這樣的疑問,為什么 PAWS 檢查要放過過期的 RST 報文。我翻了 RFC 1323 ,里面有一句提到:
It is recommended that RST segments NOT carry timestamps, and that RST segments be acceptable regardless of their timestamp. Old duplicate RST segments should be exceedingly unlikely, and their cleanup function should take precedence over timestamps.
大概的意思:建議 RST 段不攜帶時間戳,并且無論其時間戳如何,RST 段都是可接受的。老的重復(fù)的 RST 段應(yīng)該是極不可能的,并且它們的清除功能應(yīng)優(yōu)先于時間戳。
RFC 1323 提到說收歷史的 RST 報文是極不可能,之所以有這樣的想法是因為 TIME_WAIT 狀態(tài)持續(xù)的 2MSL 時間,足以讓連接中的報文在網(wǎng)絡(luò)中自然消失,所以認為按正常操作來說是不會發(fā)生的,因此認為清除連接優(yōu)先于時間戳。
而我前面提到的案例,是因為開啟了 tcp_tw_reuse 狀態(tài),跳過了 TIME_WAIT 狀態(tài),才發(fā)生的事情。
有同學(xué)會說,都經(jīng)過一個 HTTP 請求了,延遲的 RST 報文竟然還會存活?
一個 HTTP 請求其實很快的,比如我下面這個抓包,只需要 0.2 秒就完成了,遠小于 MSL,所以延遲的 RST 報文存活是有可能的。
第二個問題
開啟 tcp_tw_reuse 來快速復(fù)用 TIME_WAIT 狀態(tài)的連接,如果第四次揮手的 ACK 報文丟失了,有可能會導(dǎo)致被動關(guān)閉連接的一方不能被正常的關(guān)閉,如下圖:
總結(jié)
tcp_tw_reuse 的作用是讓客戶端快速復(fù)用處于 TIME_WAIT 狀態(tài)的端口,相當(dāng)于跳過了 TIME_WAIT 狀態(tài),這可能會出現(xiàn)這樣的兩個問題:
- 歷史 RST 報文可能會終止后面相同四元組的連接,因為 PAWS 檢查到即使 RST 是過期的,也不會丟棄。
- 如果第四次揮手的 ACK 報文丟失了,有可能被動關(guān)閉連接的一方不能被正常的關(guān)閉;
雖然 TIME_WAIT 狀態(tài)持續(xù)的時間是有一點長,顯得很不友好,但是它被設(shè)計來就是用來避免發(fā)生亂七八糟的事情。
《UNIX網(wǎng)絡(luò)編程》一書中卻說道:TIME_WAIT 是我們的朋友,它是有助于我們的,不要試圖避免這個狀態(tài),而是應(yīng)該弄清楚它。