多久算太久?估算來回時間
在第 3 篇我們看著壅塞視窗一呼一吸——在慢啟動裡衝高、在壅塞避免裡緩緩攀爬、一遇到丟失就被砍。但整台機器都懸在我們跳過的一個安靜問題上:寄件者究竟怎麼知道一個封包不見了?誠實的答案是:網路從不送來「你的封包死了」這種訊息。寄件者只能推論丟失,而它主要的線索是沉默——一個本該回來、卻沒回來的確認。所以我們最先需要的,是一種感覺:要等多久,這份沉默才算數成丟失。
那段等待時間就是重送逾時(RTO),設定它是一場真正的拉鋸。設得太短,寄件者會為只是比較慢的封包而慌張,重送仍在途中的資料、浪費容量。設得太長,寄件者會在真正丟失後乾等,讓鏈路靜下來。訣竅是把逾時錨定在這條連線量到的來回時間(RTT)上——也就是從送出一個區段到收到它的 ACK 之間的時間——因為 RTO 應該比一次正常的來回稍長一點,剛好足以確定那個 ACK 不是只是還在路上。
但 RTT 是個移動的標靶:佇列時滿時空、路線會變,所以每一次來回都不一樣。TCP 不信任任何單一次量測。它保留一個持續更新的 RTT 平滑平均值,而且同樣重要地,保留一個持續更新的「RTT 抖動幅度」估計——也就是它的變異量。逾時於是大致這樣構成:平滑後的 RTT 加上那份變異量的一個健康倍數。在穩定的鏈路上逾時緊貼平均;在抖動的鏈路上它會自動長出更寬的安全邊際,好讓一次正常的尖峰不致觸發多餘的重送。網路教會了 TCP 它自己的節奏。
快車道:重複 ACK 與快速重傳
等逾時觸發是發現丟失的慢路徑——一次完整的 RTO 可能是 RTT 的好幾倍長,而且更糟的是,計時器逾時被當成最壞的那種丟失訊號,把視窗一路砍回慢啟動。對單單一個掉落的封包而言,這是個殘酷的懲罰。TCP 想要一個更快、更溫和的方式來察覺某個區段掉了,而它在接收端自己的碎碎念裡找到了答案。
回想傳輸層那一階:TCP 的 ACK 是累積的——一個 ACK 點名接收端接下來想要的位元組。當某個區段不見了、但後面的區段仍持續抵達,接收端無法前進——於是每當又一個亂序區段落地,它就重複同一個 ACK 號,每一個都是一聲小小的呼喊:「我還在等第 N 號位元組。」如果寄件者看到同一個位元組的三個這樣的重複 ACK(要三個,這樣一點點亂序就騙不了它),那是個強烈訊號:第 N 號那個區段丟了,但它後面的流量還在流動。於是寄件者立刻重送那一個區段,不等計時器。這就是快速重傳。
sender bytes: 1001 2001 3001 4001 (each segment = 1000 bytes)
X (1001-2000 LOST)
receiver gets: 2001 3001 4001
receiver ACKs: ack1001 ack1001 ack1001 ack1001
^ ^---- 3 duplicate ACK 1001s ----^
original sender: "1001 is gone" -> RESEND 1001
(no need to wait for the timeout)輕踩煞車:快速恢復
快速重傳很快把丟失補好了,但還有第二個決定:視窗該多用力煞?在第 2 篇我們認識了 AIMD 法則——加法遞增、乘法遞減,那位溫和加速、用力煞車的小心駕駛。逾時是用力煞:網路可能真的出大事了,所以視窗一把摔回 1、重啟慢啟動。但三個重複 ACK 講的是另一個故事。接收端居然還在送 ACK,這件事本身就代表封包還在通過——管子是壅塞,不是崩潰。把這當成全面斷線會是反應過度。
於是快速恢復走較柔和的路。寄件者不掉回 1、也不重進慢啟動,而是把視窗減半(那個乘法遞減),然後直接從減半後的高度續入壅塞避免——那段溫和的加法攀爬。快速重傳搭配快速恢復這一對,是 TCP Reno 的核心,這個經典變體把殘酷的鋸齒變得平滑許多:單一丟失現在只讓你損失一半視窗,而不是全部。
要記住的圖像是:兩種丟失對上兩種反應。幾個重複 ACK 代表「有個封包從忙碌的佇列裡漏掉了」——快速重傳加快速恢復,減半後繼續巡航。一次完整的逾時代表「我什麼都沒聽到」——那可能是嚴重堵塞或路徑斷了,所以一路退回慢啟動、從頭重建。兩種情況目標相同,但沉默的嚴重程度決定了煞車的嚴重程度。
現代引擎:Reno、CUBIC 與 BBR
Reno 那段溫和的線性攀爬,對 1990 年代的網路還算夠用,但放到今天又肥又長的鏈路上就老態畢露。原因是算術:丟失之後,加法遞增每個 RTT 大約只加一個區段,所以一條橫跨高速、跨洲路徑的連線,可能要花很久才能把視窗爬回鏈路真正塞滿的高度。Reno 禮貌地一吋一吋往上挪時,鏈路的容量就閒置在那。現代變體的存在,多半就是為了修這個「填得太慢」的問題。
TCP CUBIC 是 Linux 與 Android 上的預設,所以它扛著全世界流量極大的一塊。它的點子很巧:不把視窗當成時間裡的一條直線來成長,而是沿著一條三次曲線成長。丟失剛過後它快速衝回先前的視窗大小附近,接著在逼近那個記住的天花板時謹慎地放平,然後——若沒有丟失出現——再溫和地往上探,找更多空間。這個形狀讓 CUBIC 把肥管子填滿的速度遠快於 Reno,同時仍在危險區附近收手。關鍵是:CUBIC 仍然是以丟失為本——它把一個掉落的封包讀成「管子滿了」的訊號。
來自 Google 的 BBR 打破了整個傳統。它不等丟失,而是主動量兩件事——鏈路可用的頻寬,以及它的最小來回時間——並瞄準一個剛好「填滿管子、卻幾乎清空佇列」的發送速率。它以速率為本、以模型為本,而非以丟失為本。回報是:BBR 能無視那些騙倒 CUBIC 與 Reno 的隨機、非壅塞丟失,這在容易丟包的無線路徑上是件大事。誠實的但書:因為 BBR 不像以丟失為本的流量那樣一遇丟失就退讓,把它和 CUBIC 混在同一個瓶頸裡可能不公平,要把這個平衡調對至今仍是進行中的工程課題。
當丟失說謊:無線,以及接下來
所有以丟失為本的做法都懸在一個假設上:掉一個封包代表佇列滿了,也就是壅塞。在有線鏈路上這通常成立。但在無線鏈路上,封包常死於無線電干擾、訊號太弱、或一瞬間的衰落——和壅塞完全無關。經典 TCP 分不出差別,於是把丟失讀成「網路滿了」並用力煞車,在一條其實好端端的鏈路上把吞吐量拖低。這就是以丟失為本的壅塞控制在 Wi-Fi 與行動網路上誤判的核心原因,也是 BBR「用量的、別用猜的」做法在那裡如此吸引人的一大部分理由。
這裡潛藏著一個更深的真相。丟失是個又晚又粗糙的訊號——等到一個封包掉落時,某處的佇列早已填過又空過,其他流量也早已嚐到延遲。一個更誠實的訊號會是那不斷上升的延遲本身,或是路由器在緩衝區溢位前主動打上的一個明確標記。第二個點子有個我們下一篇就要見到的名字:明確壅塞通知(ECN)讓路由器設一個旗標說「我快滿了」,好讓寄件者及早收手、而完全不必丟掉任何東西。這是壅塞控制從「推論出有麻煩」走向「被告知有麻煩」的一步。
退一步看,這條弧線就清楚了。這一階我們從區分流量控制(保護慢的接收端)與壅塞控制(保護共享的網路)開始,認識了壅塞崩潰與 AIMD 這帖解藥,看過慢啟動與鋸齒,又在這裡調校了逾時、用快速重傳加快丟失偵測、用快速恢復軟化煞車,並巡視了現代引擎。最後一篇把迴路收攏:bufferbloat、ECN 與主動佇列管理這些路由器端的幫手,以及把一切綁在一起的那個問題——當許多流量共用一條鏈路時,這個分法公平嗎?