JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

慢啟動、壅塞避免,與鋸齒波形

第二篇給了我們 AIMD 的想法——溫和加速、用力煞車。這篇要看 TCP 實際運行的那台機器:一個壅塞視窗,一開始盲目試探,接著小心翼翼地爬升,在你「速度對時間」的圖上畫出那條著名的鋸齒線。

第二個視窗:我可以送多快?

在第一篇你認識了接收視窗——接收端對「自己能裝多少」許下的承諾。在第二篇你認識了 AIMD 的想法——那份讓網際網路不會熔毀的社會契約。現在我們要把這份契約裝成寄件者體內一個真正的齒輪。TCP 維護一個私有的數字,通常寫作 cwnd,叫做壅塞視窗。它是寄件者自己對「此刻網路——而不是接收端——能安全吞下多少未確認資料」的猜測。接收端永遠看不到它;它完全活在寄送這一側。

所以一個 TCP 寄件者同時被兩個上限綁住,而它必須服從較小的那一個。任一時刻,它最多能有 min(cwnd, 接收視窗) 個位元組在途——尚未被確認。把它想成同一根管子上的兩個閥門:接收視窗是接收端的水桶允許的寬度,壅塞視窗是網路允許的寬度,水流取決於關得比較緊的那個閥。這篇完全在談第二個閥:當沒有人告訴 TCP 網路真正的容量時,它怎麼決定 cwnd。

慢啟動:其實不慢,而是從零開始的快速試探

當一條連線全新時,寄件者對這條路徑一無所知。轟出一大串巨量資料,可能瞬間塞爆某個路由器、觸發你在第二篇認識的壅塞崩潰。所以 TCP 從很小開始——cwnd 只有幾個區段——再往上爬。這個階段叫做 慢啟動(slow start),這名字會誤導人:它其實讓視窗呈指數成長。規則漂亮地簡單:每當一個 ACK 確認了一個區段,就把 cwnd 增加大約一個區段。因為每個來回都會回來整整一個視窗份量的 ACK,視窗便在每個來回時間翻一倍。

  1. 第 0 回合:cwnd = 1 個區段。送出 1 個區段,等一個 RTT 拿到它的 ACK。
  2. 第 1 回合:那 1 個 ACK 把 cwnd 推到 2。送 2 個,拿回 2 個 ACK。
  3. 第 2 回合:cwnd = 4。第 3 回合:cwnd = 8。第 4 回合:cwnd = 16——每個 RTT 翻倍。
  4. 這個攀升一直持續,直到 cwnd 達到一個門檻(叫做 ssthresh),或發生丟失——到那一刻規則就改變,我們接下來會看到。

若目標是謹慎,為什麼要指數成長?因為未知的容量可能極大,而每個來回只爬一個區段,會讓一條快鏈路半空地閒置好幾分鐘。翻倍只要少少幾個來回就能抵達正確的鄰近區域——要達到 64 個區段的視窗,大約只需 6 個 RTT,而不是 64 個。慢啟動其實是快速試探:迅速進到大致範圍,再在你衝過頭、撞毀之前,交棒給一個更溫和的階段。

壅塞避免:小心的線性攀升

永遠翻倍本身就會造成撞毀——你會在單一個來回內衝過這條路徑的容量。所以一旦 cwnd 越過門檻 ssthresh,TCP 就換檔進入壅塞避免。現在成長是線性的、不再是指數的:不論回來了多少個 ACK,cwnd 每個來回只升大約一個區段。這就是 AIMD 裡「加法增大」的具體化身——一位已經接近速限、只輕點油門的小心駕駛那種溫和、有耐心的加速。

這兩個階段共享一個目的,膽量卻不同。慢啟動說:「我完全不知道能跑多快,那就趕快找出來。」壅塞避免說:「我現在大概接近極限了,那就每趟只多踩一點點,盯著第一個出問題的徵兆。」第一個出問題的徵兆是丟失——一個沒回來的 ACK,或更快的:觸發快速重傳的重複 ACK 模式(這個我們在第四篇才會好好打開)。當丟失出現,AIMD 的「乘法減小」那一半就發動:cwnd 被大幅砍掉——大約砍半——再從那裡重新開始攀升。

鋸齒波形:AIMD 隨時間看起來的樣子

現在把這些拼塊放上一張圖:底下是時間,側邊往上是 cwnd。線性增大畫出一條緩慢上升的斜坡;一次丟失畫出一道突然垂直、掉到大約一半的落差;接著斜坡重新開始。重複下去,你就得到一排傾斜的牙齒——著名的 TCP 鋸齒波形。它不是哪裡壞掉的徵兆,而正是健康的壅塞控制該有的樣子:TCP 永遠在溫和地試探更多頻寬、找到天花板、退讓、再試探。這條連線活在一個推進與退讓的、有產出的循環裡。

cwnd
 |              /|           /|           /|
 |            /  |         /  |         /  |
 |          /    |       /    |       /    |
 |        /      v     /      v     /      v   <- loss: cut ~in half
 |      /         \  /         \  /
 |    /            \/           \/
 |  /  slow start   linear climb (congestion avoidance)
 |/_____exponential__________________________________ time
     (doubles each RTT, until ssthresh or first loss)
指數的慢啟動發動攀升;接著線性上升與砍半反覆出現,畫出鋸齒波形。

鋸齒波形也悄悄解釋了公平性,這條線第五篇會去拉。共享同一個瓶頸的兩條流,都以每個 RTT 一個區段的速度成長(相同的加法步伐),也都在丟失時砍掉相同的比例(相同的乘法步伐)。「加相同的量、減一個比例」的數學,會隨時間把不平等的流推向平等的份額。沒有中央裁判去分配這個比例——它從每個寄件者各自獨立遵守同一條簡單規則中浮現出來。這就是 AIMD 安靜的天才,在那些牙齒裡被看見。

誠實的極限:當鋸齒波形是錯的形狀時

我們剛畫的那條經典鋸齒,屬於 TCP Reno,那個教科書式、以丟失為訊號的方案。它能用,但在現代網際網路上有真實的弱點,誠實面對這些弱點正是「精通」的全部意義。在一條又肥又長距離的鏈路上——高頻寬延遲乘積——Reno 每個 RTT 一個區段的攀升,在每次砍半後要重新填滿管子慢得令人受不了,因而可能讓一條快路徑被嚴重低度使用。這就是為什麼 CUBIC(今天 Linux 和 Android 上常見的預設)把那條筆直的斜坡,換成一條先快速衝回先前天花板、接近時再放緩的曲線。

還有一個更深的假設值得質疑。整個鋸齒波形都建立在「把丟失讀成壅塞的訊號」之上——而這在兩處會出錯。在 Wi-Fi 或行動網路上,封包常死於無線電干擾、而非佇列滿了,但以丟失為訊號的 TCP 照樣退讓、吞吐量便下垂(你在傳輸層那一級看過這個提醒)。反過來說,今天過大的路由器緩衝區能在不丟任何東西的情況下吸納龐大的積壓,於是丟失來得很晚,而延遲卻悄悄膨脹——這就是第五篇要正面處理的緩衝膨脹(bufferbloat)問題。較新的設計像 BBR,試著直接對路徑的頻寬與來回延遲建模、而不等丟包,藉此繞過這兩個問題。

也要把分層看得誠實。慢啟動、壅塞避免與鋸齒波形,全都是寄件者這一側、純粹用普通 ACK 建起來的行為——TCP 從回來的那一小股確認,推斷出對整條路徑的判決。在經典 TCP 裡,網路從不明確地說「慢下來」。第五篇會介紹現代的例外,那時路由器能在封包上做記號、正是說出這句話;而第四篇會放大丟失偵測的細節——逾時與快速重傳——它們決定鋸齒波形每一顆牙齒往下轉的精確時刻。