斯坦福 CS144 計算機網絡播客:TCP 擁塞控制
在計算機網絡的世界里,數據包就像高速公路上飛馳的汽車。當車流量恰到好處時,一切井然有序;但如果車輛瞬間涌入,遠超道路承載能力,就會發生嚴重的堵車。網絡中的“堵車”就是我們今天要討論的核心話題—— 擁塞(Congestion) 。
本文將以 Stanford CS 144 課程的核心知識為基礎,帶你深入理解 TCP 擁塞控制的來龍去脈,從它是什么、為什么重要,到核心算法 AIMD,再到 TCP 各個版本的演進。
什么是擁塞控制?
首先,我們需要區分一個非常相似但本質不同的概念: 流量控制 (Flow Control) 。流量控制是點對點的問題,它確保發送方不會因為發送速度過快而淹沒接收方。這好比兩個人對話,一方說得太快,另一方需要讓他“慢一點”。
而 擁塞控制 (Congestion Control) 則是一個全局性的問題。它關注的是整個網絡的承載能力,確保數據發送方不會向網絡中注入過多的數據,導致路由器等網絡設備不堪重負。它管理者是“所有發送者”與“網絡”之間的關系。
簡單來說,擁塞控制是一種資源管理機制,它的目標是:
- 防止網絡過載 :避免路由器緩沖區溢出和大規模的數據包丟失。
- 保證資源公平分配 :讓多個數據流(flow)可以相對公平地共享有限的網絡帶寬。
有趣的是,IP 協議本身并不提供擁塞反饋,因此,擁塞控制的重任落在了傳輸層,主要由 終端主機 (end-hosts) 上的 TCP 協議來完成。這正是著名的“端到端原則”的一個經典體現。但這又引申出另一個問題:端是不知道網絡中各節點的狀態的,因此想在端到端控制網絡擁塞是一個充滿挑戰的任務。
擁塞的危害:從性能下降到網絡崩潰
當發送方注入網絡的數據量超過了某個瓶頸路由器的處理能力時,數據包就會開始在路由器的緩沖區中排隊。適度的排隊是正常的,它表明網絡鏈路得到了充分利用。
然而,當數據注入速度持續高于處理速度時,路由器的緩沖區最終會被填滿,后續到達的數據包將被無情地丟棄。這就是 丟包 (Packet Loss) 。
丟包會觸發發送方的重傳機制,但這會導致一個惡性循環:
- 網絡已經擁塞,發生丟包。
- 發送方檢測到丟包,認為數據未送達,于是重傳數據。
- 重傳的數據包進一步加劇了網絡的擁塞。
在極端情況下,這可能導致 擁塞崩潰 (Congestion Collapse) ——網絡中充斥著大量重傳的數據包,但幾乎沒有任何有效的數據能夠成功傳輸到目的地,網絡吞吐量急劇下降,甚至趨近于零。
理想模型:帶寬時延積 (BDP)
那么,一個理想的發送方應該以多快的速度發送數據呢?TCP 引入了一個名為 擁塞窗口 (Congestion Window) ,通常寫作 cwnd 的變量來控制。它表示在收到確認(ACK)之前,可以發送到網絡中的最大數據量。
注意:實際的發送窗口大小是
cwnd和接收方通告的接收窗口rwnd中的較小值,即EffectiveWindow = min(cwnd, rwnd)。
在理想狀態下,我們希望 cwnd 的大小恰好能“填滿”從發送方到接收方的網絡路徑。這個理想值就是 帶寬時延積 (Bandwidth-Delay Product, BDP) 。
BDP 的計算公式為:
它代表了在網絡路徑中可以容納的最大數據量。我們可以用一個簡單的圖來理解:
Sender Receiver
| |
|------------------ The "Pipe" of the Network ---------------|
| |
v v
<------------------ RTT (Round-Trip Time) ------------------->
Data in flight to fill the pipe = Bandwidth * RTT例如,如果一條鏈路的帶寬是 5 Mbit/s,往返時延(RTT)是 100 ms,那么:
BDP = 5,000,000 bits/s * 0.1 s = 500,000 bits = 62,500 bytes = 62.5 KB
這意味著,為了讓鏈路時刻保持繁忙而又不產生排隊,發送方需要維持一個 62.5 KB 大小的擁塞窗口。
然而,現實是殘酷的。TCP 發送方在運行時 無法預知 瓶頸鏈路的帶寬、網絡中是否存在其他競爭流量,以及沒有排隊時的最小 RTT。因此,TCP 需要一個動態的算法來探測并逼近這個理想的 cwnd 值。
AIMD:TCP 擁塞控制的基石
為了動態調整 cwnd,TCP 采用了一種被稱為 加性增窗、乘性減窗 (Additive Increase, Multiplicative Decrease, AIMD) 的經典算法。
這個算法的邏輯非常優雅,可以概括為“謹慎探測,快速避讓”。
加性增窗 (Additive Increase, AI)
當網絡狀態良好時(即數據包被成功確認),TCP 會認為網絡仍有余力,于是緩慢地增加其擁塞窗口,以探測更多的可用帶寬。
具體來說,在擁塞避免階段,每經過一個 RTT,如果所有數據包都被成功確認,cwnd 就會增加一個 最大段大小 (Maximum Segment Size, MSS) 。
實現細節:為了在每個 RTT 內增加 1 個 MSS,TCP 的策略是每收到一個 ACK,cwnd 就增加 MSS / cwnd。這樣,在一個窗口的數據被全部確認后,cwnd 大小正好增加了約 1 個 MSS。詳細解釋一下:
- 目標: 在擁塞避免階段,每經過一個 RTT,
cwnd增加 1 個 MSS。 - 挑戰: 發送方并不知道 RTT 的精確時長。它只能通過“收到一個窗口數據的 ACK”來近似一個 RTT 周期。
- 推導:
假設當前 cwnd 是 10 個 MSS。這意味著在一個 RTT 內,發送方會發出 10 個數據段,并期望收到 10 個對應的 ACK。
我們的目標是在收到這 10 個 ACK 后,cwnd 的總增量為 1 個 MSS。
那么,將這個總增量平均分配到每個到來的 ACK 上,就是最自然的方法。
每個 ACK 應該帶來的增量 = 總增量 / ACK 數量 = 1 MSS / 10。
這里的 10 正好是當前 cwnd 能容納的段數,即 cwnd / MSS。
所以,每個 ACK 帶來的增量 = 1 MSS / (cwnd / MSS) = MSS * MSS / cwnd (如果 cwnd 以字節為單位),或者更通俗地寫成 MSS / cwnd。
- 結論: 這個公式
cwnd += MSS / cwnd是一種平滑的、在 RTT 周期內實現+1 MSS目標的工程實現。
乘性減窗 (Multiplicative Decrease, MD)
當網絡發生擁塞(即檢測到丟包)時,TCP 必須迅速降低其發送速率,以緩解網絡壓力。這個反應必須是劇烈的。
TCP 的策略是將 cwnd 直接 減半 (cwnd = cwnd / 2)。這種大幅度的削減能夠快速為網絡“降溫”,為恢復穩定創造條件。
“鋸齒”形的 cwnd 變化
AIMD 算法導致 cwnd 的變化呈現出一種非常典型的“鋸齒狀”模式。

這種不斷探測(增加)和快速后退(減半)的機制,使得 TCP 能夠動態地適應網絡負載的變化,并在多個 TCP 流之間實現相對的公平。
然而,AIMD 也有一個固有的問題:它的吞吐量與 1/√p(p 是丟包率)成正比,與 1/RTT 成正比。這意味著,RTT 越長的連接(例如跨國連接),其吞吐量會越低,因為它增加 cwnd 的速度更慢。這在一定程度上懲罰了距離較遠的連接。
- 為什么與
1/RTT成正比? 這個比較好理解。吞吐量的基本定義是窗口大小 / 時間。在這里,就是cwnd / RTT。在一個 RTT 時間內,我們最多可以發送cwnd大小的數據。因此,RTT 越長,單位時間能發送的輪次就越少,吞吐量自然就越低。 - 為什么與
1/√p成正比? (p 是丟包率)
想象一下 AIMD 的“鋸齒”圖。cwnd 從 W/2 增長到 W 時發生一次丟包。
在一個丟包周期內,發送的數據包總數約等于這個鋸齒下方的面積。這個面積大致和 W2 成正比。
丟包率 p 的含義是“平均每發送 1/p 個包,就會發生一次丟包”。
因此,一個丟包周期內發送的數據包總數也約等于 1/p。
所以我們得出 W2 ∝ 1/p,進而 W ∝ 1/√p。
因為吞吐量正比于平均窗口大小,而平均窗口大小又正比于最大窗口 W,所以 吞吐量 ∝ 1/√p 。
- 綜合來看: 這個公式揭示了標準 TCP 的一個內在特性:在有損網絡中,丟包率對吞吐量的影響是平方根級別的;而在高延遲網絡中,RTT 對吞吐量的影響是線性的。這也解釋了為什么長距離(高 RTT)連接在有少量丟包時性能下降如此明顯。
慢啟動:快速啟動,指數增長
如果一個新的 TCP 連接從一個很小的 cwnd (例如 1-3 個 MSS) 開始,使用 AIMD 的加性增長,那么它需要很多個 RTT 才能達到一個合理的窗口大小,這個過程太慢了。
為了解決這個問題,TCP 引入了 慢啟動 (Slow-Start) 機制。雖然名字里有“慢”,但它的增長方式實際上是 指數級 (exponential) 的,非常快。
為什么叫慢啟動呢? 這是一個歷史遺留的命名問題,關鍵在于“和誰比”。在“慢啟動”算法被發明之前,TCP 連接一旦建立,發送方會立即向網絡中注入接收方窗口 (rwnd) 允許的全部數據。如果網絡中間某個路由器處理不了這么大的瞬時流量,就會立即導致擁塞和大量丟包。
相比于那種“瞬間洪水”式的野蠻行為,從 cwnd=1 開始,然后以指數級增長的方式“慢慢地”將數據注入網絡,已經是一種非常“溫柔”和“緩慢”的啟動方式了。所以,“慢啟動”的“慢”是相對于過去那種簡單粗暴的實現而言的。
- 初始階段 :連接建立后,
cwnd被初始化為一個較小的值,例如IW(Initial Window),通常為 2-4 個 MSS,在現代系統中甚至可以達到 10 個 MSS。 - 指數增長 :在慢啟動階段,每收到一個 ACK,
cwnd就會增加 1 個 MSS。這意味著,每經過一個 RTT,cwnd就會 翻倍 (具體可以參考下面的例子)。 - 退出條件 :慢啟動階段會在以下兩種情況之一發生時結束:
當 cwnd 的值超過一個預設的 慢啟動閾值 (slow-start threshold, ssthresh) 時,TCP 會從慢啟動階段切換到擁塞避免階段(即 AIMD 的加性增長)。
當檢測到丟包時。
慢啟動的目的是在連接初期快速探測網絡的可用容量,盡快將 cwnd 提升到一個合理的量級,然后再轉入更為謹慎的 AIMD 階段。
MSS (Maximum Segment Size) 的確定
MSS 并不是一個固定的常量,而是在 TCP 連接建立的“三次握手”階段,由通信雙方 協商 確定的。它的核心目標是盡可能地利用網絡鏈路的承載能力,同時避免 IP 層的數據包分片。
MSS 的大小受到鏈路層最大傳輸單元 MTU (Maximum Transmission Unit) 的限制。其理想計算公式為:
MSS = MTU - IP Header Size - TCP Header Size
協商過程:
- 在三次握手時,客戶端在第一個
SYN包中,通過 TCP 選項字段宣告自己的MSS值。 - 服務器在返回的
SYN-ACK包中,也會宣告自己的MSS值。 - 最終,雙方會選擇兩者中 較小 的
MSS值作為本次連接的最終MSS。這確保了任何一方發送的數據段都不會超過對方的處理能力。
生產中的典型值:
- 在最常見的以太網(Ethernet)環境中,
MTU通常是 1500 字節。 - 標準的 IP 頭部大小為 20 字節,TCP 頭部大小也為 20 字節(不含選項)。
- 因此,最常見的
MSS值為:1500 - 20 - 20 = 1460 字節。 - 如果你在使用撥號上網(PPPoE),它會額外占用 8 字節,導致
MTU為 1492 字節,MSS則為 1452 字節。VPN 或其他隧道技術也可能進一步減小MTU,從而影響MSS的取值。
現代操作系統通常會自動處理這個協商過程。此外,還有一種更高級的機制叫 路徑 MTU 發現 (Path MTU Discovery, PMTUD) ,它能動態地發現從源到目的地的整個路徑上最小的 MTU,從而更精確地設置 MSS,但這已超出了基礎討論的范疇。
ssthresh (Slow Start Threshold) 的確定
與 MSS 不同,ssthresh 是 TCP 發送方 內部維護 的一個狀態變量,它不對外宣告,也無需協商。它的作用是作為慢啟動和擁塞避免兩種模式的切換“警戒線”。
初始值: 在一個 TCP 連接剛剛建立時,發送方對網絡路徑的狀況一無所知。因此,ssthresh 通常會被設置為一個 非常大 的值(實際上是系統允許的最大接收窗口大小)。這樣做是為了讓“慢啟動”階段可以盡可能地發揮作用,不受限制地進行指數增長,直到第一次擁塞事件發生。
動態更新:ssthresh 的值在連接的整個生命周期中是動態變化的,其更新遵循一個核心原則: 當擁塞發生時,將 ssthresh 設置為當前擁塞窗口 cwnd 的一半 。
- 更精確地說,是
ssthresh = max(FlightSize / 2, 2 * MSS)。FlightSize指的是已發送但未被確認的數據量,在大多數情況下約等于cwnd。 - 與
2 * MSS取較大值的目的是為了防止ssthresh變得過小,導致網絡性能低下。
ssthresh 的本質是 TCP 的一種“記憶”: 它記錄了上一次網絡擁塞發生時窗口大小的一半,作為下一次探測網絡容量時的“安全”起點 。
TCP 擁塞控制實戰:一個完整例子的剖析
理論總是有些枯燥,讓我們通過一個生動的例子,從頭到尾觀察一個 TCP 連接的 cwnd 是如何在各種算法的支配下動態變化的。
場景設定:
- 客戶端 A 向服務器 B 發送一個大文件。
- 我們假設 MSS = 1 KB,方便計算。
- 連接剛建立,初始擁塞窗口
cwnd= 2 MSS (即 2 KB)。 - 初始慢啟動閾值
ssthresh非常大 (例如 64 KB),在開始階段不會限制cwnd的增長。
第 1 階段:慢啟動 (Slow-Start)
連接開始,TCP 首要任務是快速探明網絡容量。
- RTT 0:
cwnd = 2 KB。A 發送 2 個數據包。 - RTT 1: A 收到 2 個 ACK。根據慢啟動規則(每收到一個 ACK,
cwnd增加 1 MSS),cwnd變為2 KB + 2 * 1 KB = 4 KB。A 現在可以發送 4 個數據包。 - RTT 2: A 收到 4 個 ACK。
cwnd變為4 KB + 4 * 1 KB = 8 KB。 - RTT 3: A 收到 8 個 ACK。
cwnd變為8 KB + 8 * 1 KB = 16 KB。 - RTT 4: A 收到 16 個 ACK。
cwnd變為16 KB + 16 * 1 KB = 32 KB。
cwnd 的增長軌跡是 2 -> 4 -> 8 -> 16 -> 32,這是典型的指數增長。假設我們的 ssthresh 初始值就是 32 KB。現在 cwnd 達到了 ssthresh,慢啟動階段結束。
第 2 階段:擁塞避免 (Congestion Avoidance)
cwnd 已經到達了一個相對合理的水平,不能再野蠻增長了,需要進入謹慎的線性探測階段。
- RTT 5:
cwnd = 32 KB。A 發送 32 個包并全部收到 ACK。根據擁塞避免規則(每輪 RTT 增加 1 MSS),cwnd變為32 KB + 1 KB = 33 KB。 - RTT 6:
cwnd = 33 KB。一切順利。cwnd變為33 KB + 1 KB = 34 KB。 - ... 這個過程持續進行,
cwnd緩慢增加。 - RTT n: 假設當
cwnd增長到40 KB時,網絡開始出現擁塞,A 發送的 40 個包中有一個不幸丟失了。
第 3 階段:擁塞事件 - 三次重復確認 (Reno 模式)
接收方 B 沒有收到丟失的包,但收到了它后面的一些包。于是 B 不斷地對最后一個收到的、有序的包發送重復 ACK。很快,A 收到了三次重復的 ACK。
- 擁塞信號: 三次重復確認。這被認為是輕度擁塞的信號。
- 觸發機制: 快速重傳 (Fast Retransmit) 與快速恢復 (Fast Recovery)。
- 變量變化:
- A 立即重傳丟失的那個包,不等待超時。
ssthresh被更新為當前cwnd的一半:ssthresh = 40 KB / 2 = 20 KB。這是新的“慢啟動警戒線”。cwnd不再降為 1,而是直接設置為新的ssthresh值:cwnd = 20 KB。- TCP 直接進入 擁塞避免 階段。
第 4 階段:新一輪的擁塞避免
A 從 cwnd = 20 KB 開始,繼續進行線性的加性增窗。
- RTT n+1:
cwnd = 20 KB。發送 20 個包,一切順利。cwnd變為20 KB + 1 KB = 21 KB。 - RTT n+2:
cwnd = 21 KB。cwnd變為21 KB + 1 KB = 22 KB。 - ...
cwnd再次緩慢爬升。
第 5 階段:嚴重擁塞事件 - 超時 (Timeout)
假設當 cwnd 增長到 26 KB 時,網絡發生了嚴重的擁塞,A 發送的一個數據包及其重傳包都丟失了,導致 A 在規定時間內沒有收到任何 ACK,觸發了 超時 (Timeout) 。
- 擁塞信號: 超時。這被認為是嚴重擁塞的信號。
- 變量變化:
ssthresh更新為當前cwnd的一半:ssthresh = 26 KB / 2 = 13 KB。cwnd被無情地重置為初始值:cwnd = 2 KB。- TCP 重新進入 慢啟動 階段。
TCP Congestion Control 經典圖例:


解惑:ACK 時鐘:TCP 如何感知“一輪” RTT?
TCP 沒有 一個精確的“RTT 時間窗”來框定“這一輪”的 ACK。它依賴于一個更為優雅和強大的機制—— ACK 時鐘 (ACK Clocking) 。
核心思想:事件驅動,而非時間驅動
首先要明確,TCP 的擁塞控制算法不是基于一個固定的計時器去判斷“一輪 RTT 是否結束”。它是一個 事件驅動 的系統,這個核心事件就是 收到 ACK 。
這個機制被稱為“自時鐘 (Self-clocking)”。可以這樣理解:
- 發送方將一個窗口的數據包(例如
cwnd個)發送到網絡中。 - 這些數據包在網絡中傳輸,經過路由、排隊,最終到達接收方。
- 接收方在收到數據后,會返回 ACK。
- 這些 ACK 同樣需要經過網絡傳輸才能回到發送方。
- 關鍵在于: 發送方只有在收到一個“舊”數據包的 ACK 后,才被允許發送一個“新”的數據包。
ACK 返回的速率,從根本上決定了發送方發送新數據的速率。如果網絡通暢,ACK 回來得快,發送方的“時鐘”就走得快,數據發送也快。如果網絡擁塞,ACK 回來得慢,發送方的“時鐘”自然就慢下來了,數據發送也隨之減速。
如何實現“每輪 RTT”的規則?
既然沒有精確的 RTT 時間窗,那“每輪 RTT 增加 1 MSS”這種規則是如何實現的呢?
TCP 將“ 成功接收到一個 cwnd 大小數據的 ACK ”近似地看作“ 經過了一輪 RTT ”。
讓我們回到擁塞避免的例子:
- 假設
cwnd = 32 KB,MSS = 1 KB。這意味著發送方在一個“RTT 周期”內,期望收到 32 個數據包的 ACK。 - “每輪 RTT 增加 1 MSS”的目標,就被分解到了這 32 個 ACK 事件上。
- 每當收到一個 ACK,
cwnd就增加一小部分:1 MSS / 32。 - 當所有 32 個 ACK 都順利返回時,
cwnd的總增量正好是32 * (1 MSS / 32) = 1 MSS。 - 這正是
cwnd += MSS * MSS / cwnd公式(當cwnd以字節為單位時)的直觀體現。
結論就是: TCP 不去“判斷”一輪的 ACK 是否全部收到了,而是通過一個 平滑、累加 的方式,將宏觀的“每輪 RTT”的目標,分解到微觀的“每一次 ACK 到達”的事件上。ACK 的到達自然地驅動著窗口的增長,構成了 TCP 擁塞控制算法的脈搏。
解惑:詳解快速重傳與快速恢復
這兩個機制是 TCP Reno 相對于 TCP Tahoe 的核心改進,它們緊密協作,旨在更智能地處理輕度網絡擁塞。
快速重傳 (Fast Retransmit):擁塞的“哨聲”
快速重傳是一種 丟包檢測 機制。它的核心思想是,如果發送方連續收到了 三個或以上 的重復 ACK(例如,都要求確認同一個序列號),那么 TCP 就有充分的理由相信,這個序列號對應的數據包已經在網絡中丟失了。
為什么是三次? 網絡中的數據包有時會因為路由變化等原因發生 亂序 。收到一兩個重復的 ACK 可能是由于亂序到達引起的,但收到三個就說明后續的數據包大概率已經到達,而中間的那個包丟失的可能性極高。
一旦觸發快速重傳,發送方會 立即重傳 那個被認為丟失的數據包,而 不必等待超時計時器 (RTO) 到期 。這就是“快速”的含義。它大大縮短了因丟包而造成的傳輸中斷時間。
快速恢復 (Fast Recovery):擁塞后的“手術刀”
快速恢復是一個 擁塞控制 策略。它在快速重傳被觸發后 立即啟動 。
它做什么?(Reno 的經典流程)
- 進入狀態: 當快速重傳觸發時,TCP 進入快速恢復狀態。
- 執行操作:
- 將
ssthresh設置為當前cwnd的一半(ssthresh = cwnd / 2)。這步操作被稱為“乘性減窗 (MD)”。 - 將
cwnd也直接設置為新的ssthresh值(cwnd = ssthresh)。 - 注意: 與 Tahoe 不同,
cwnd沒有降為 1,并且跳過了慢啟動。
- 維持與退出: TCP 會一直 保持 在快速恢復狀態,直到收到一個 新的 ACK (即確認了被重傳數據的 ACK)。收到這個新 ACK 后,TCP 就會 退出快速恢復 狀態,并進入標準的 擁塞避免 階段。
存在退出快速恢復的說法嗎? “快速恢復”是 TCP 的一個明確的 狀態 。TCP 在進入這個狀態后,會執行一系列特殊的操作,直到某個條件滿足后才會 退出 該狀態。
TCP 擁塞控制的演進史
TCP 擁塞控制不是一蹴而就的,而是經歷了幾十年的發展和迭代。其中最重要的幾個版本是 Tahoe、Reno 和 NewReno。
TCP Tahoe (1988) - 開創者
為了解決 1980 年代末期的“擁塞崩潰”問題,TCP Tahoe 首次引入了完整的擁塞控制機制:
- 核心機制 :同時包含了慢啟動和擁塞避免 (AIMD)。
- 丟包檢測 :通過兩種方式推斷丟包:
超時 (Timeout) :發送數據后在規定時間內未收到 ACK。
三次重復確認 (Triple Duplicate ACKs) :連續收到三個相同的 ACK,表明接收方收到了后續的數據包,但中間有一個包丟失了。
- 策略 :無論是由超時還是由三次重復確認檢測到丟包,Tahoe 的處理方式都非常“激進”:
將 ssthresh 設置為當前 cwnd 的一半。
將 cwnd 重置為 1 個 MSS。
重新進入 慢啟動 階段。
這種“一刀切”的方式雖然有效,但在網絡只是輕微擁塞(例如只丟了一個包)時,將 cwnd 降至 1 會導致吞吐量的巨大抖動。
TCP Reno (1990) - 優化者
TCP Reno 在 Tahoe 的基礎上進行了重要優化,它認為“三次重復確認”和“超時”是兩種不同程度的擁塞信號。
- 快速重傳 (Fast Retransmit) :當收到三次重復確認時,Reno 不再等待超時,而是立即重傳丟失的那個數據段。
- 快速恢復 (Fast Recovery) :這是 Reno 的精髓。在執行快速重傳后,Reno 不會 將
cwnd降至 1,而是:
將 ssthresh 設置為當前 cwnd 的一半。
將 cwnd 直接設置為新的 ssthresh 值(而不是 1)。
直接進入 擁塞避免 階段(加性增長),跳過了慢啟動。
這種機制使得在發生單個丟包時,TCP 的吞吐量下降得不那么劇烈,恢復得也更快。只有在發生更嚴重的“超時”事件時,Reno 才會像 Tahoe 一樣將 cwnd 降至 1 并重新開始慢啟動。
TCP NewReno (1996) - 完善者
TCP Reno 在處理單個窗口內發生 多個丟包 的情況時表現不佳。當第一個丟失的包被重傳并確認后,Reno 就會退出快速恢復狀態,但此時窗口內可能還有其他丟失的包,這會導致吞-吐量的多次劇烈下降。
TCP NewReno 優化了快速恢復算法,使其能夠更好地處理單個窗口內的多個丟包事件,從而提高了網絡傳輸的穩定性和效率。
Reno 的“快速恢復”有一個致命缺陷:它只能很好地處理單個窗口內的 一次 丟包。如果一個窗口內有多個包丟失了,Reno 就會“精神錯亂”。
Reno 的問題場景:
- 假設發送方在一個窗口內發送了 10 個包(#1 到 #10)。其中 #3 和 #5 都丟了。
- 接收方收到 #1, #2, #4。于是不斷發送針對 #2 的重復 ACK。
- 發送方收到三次重復 ACK,觸發快速重傳,重傳 #3,并進入快速恢復(
cwnd減半)。 - 假設重傳的 #3 成功到達。接收方現在可以確認到 #4 了,于是發回一個對 #4 的新 ACK。
- 問題來了: Reno 收到這個新的 ACK (被稱為部分 ACK,因為它沒有確認所有已發送的數據) 時,會錯誤地認為網絡擁塞已經解除,于是 立即退出快速恢復 狀態,并將
cwnd設置回ssthresh。 - 但實際上 #5 仍然是丟失的!很快,發送方又會收到針對 #4 的重復 ACK,不得不再次觸發快速重傳和快速恢復,導致
cwnd再一次減半。這使得吞吐量恢復得非常慢。
NewReno 的優化:
- NewReno 在收到部分 ACK 時, 不會立即退出快速恢復 。
- 它會判斷出“哦,我重傳的包被確認了,但對方沒有確認到最新的數據,這說明窗口里肯定還有別的包也丟了”。
- 于是,NewReno 會立即重傳它推斷出的下一個丟失的包(在這個例子里是 #5),并 保持在快速恢復狀態 ,直到所有在進入該狀態時“在途”的數據包都被確認為止。
- 這樣,NewReno 用一次快速恢復過程,解決了單個窗口內的多次丟包問題,避免了
cwnd的多次懲罰性下降。
下面是一個簡單的對比表格:
事件 | TCP Tahoe 的響應 | TCP Reno / NewReno 的響應 |
超時 (Timeout) |
, |
, |
三次重復確認 |
, |
, |
總結與展望
TCP 擁塞控制是互聯網能夠穩定運行的基石之一,它體現了分布式系統中簡單而強大的設計哲學。其核心目標可以總結為:
- 高吞吐量 (High Throughput) :高效利用網絡資源。
- 公平性 (Fairness) :確保不同數據流能公平共享帶寬。
- 快速響應 (Responsiveness) :快速適應網絡條件的變化。
- 分布式控制 (Distributed Control) :無需中央協調,易于擴展。
從 Tahoe 的誕生,到 Reno 和 NewReno 的不斷完善,TCP 擁塞控制的核心思想——基于丟包信號的 AIMD——在很長一段時間內都主導著互聯網。
然而,時代在進步。在當今高帶寬、高延遲(長肥網絡)和無線網絡環境下,僅依賴丟包作為擁塞信號已顯不足。因此,更新的擁塞控制算法,如基于延遲的 TCP Vegas 、Google 開發的 BBR (Bottleneck Bandwidth and Round-trip propagation time) 以及目前 Linux 系統默認的 CUBIC,正在不斷涌現和部署,以適應更加復雜的網絡環境。
但無論技術如何演進,理解 TCP Tahoe 和 Reno 所奠定的基礎,依然是每一位網絡學習者和工程師的必修課。


























