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

流量控制 vs 壅塞控制

兩個問題聽起來一樣,其實完全不同:一個快速的發送方可能淹沒一個慢速的接收方,而一屋子的發送方可能淹沒中間那張網路。這篇會把這兩件事乾淨俐落地拆開,並描繪出本段位接下來要建立的一切。

淹死一條連線的兩種方式

在傳輸層段位裡,你看過 TCP 如何把一張會丟失、會重排的網路變成一條完美的位元組串流:它替每一個位元組編號、確認抵達的部分、重傳沒抵達的部分。但「可靠」引出了一個它自己回答不了的問題:發送方該以什麼速度灌入位元組?太慢就浪費了鏈路;太快則位元組會在某處堆積然後被丟掉。結果有兩個完全不同的地方它們可能堆積,而 TCP 對每一個都需要一套不同的機制。

第一個地方是接收方本身。想像把水從水壺倒進杯子。杯子就是接收方的緩衝區——一塊固定大小的記憶體,抵達的資料在那裡等著應用程式去讀。如果發送方倒得比應用程式喝得快,杯子就會溢出,資料就遺失了。防範這件事的就是流量控制,它整個關注的只有一個慢速的端點:保護接收方不被一個對它而言太快的發送方淹沒。

第二個地方是中間那張網路——位元組必須穿越的那些路由器與鏈路。想像一條四線道高速公路縮成一線道:那個收窄點就是瓶頸鏈路。如果所有經由它發送的人合起來提供的流量超過瓶頸所能承載的,封包就會在某台路由器的緩衝區裡排隊,隊伍塞滿,封包就被丟掉。防範這件事的就是壅塞控制,它關注的是共享的東西:保護那張不屬於任何單一連線的網路,使其不被所有人同時使用所累加的重量壓垮。

流量控制:接收方遞給你的一個數字

流量控制是簡單的那個,因為接收方知道答案、也可以直接說出來。在它送回的每一個確認(ACK)裡,接收方都附上一個叫做接收視窗(rwnd)的欄位:它的緩衝區此刻還剩下多少位元組的空位。發送方遵守一條簡單的規則——在途中尚未被確認的資料,永遠不要多於最新通告的視窗。如果接收方說「我還有 8 KB 的空間」,發送方就最多只能有 8 KB 在外未確認,然後必須等到一個確認回來才能再送。

這正是你在連結層與傳輸層段位見過的滑動視窗,現在改由接收方的空位來驅動。當應用程式從杯子裡喝水,空位就騰出來;下一個確認就通告一個更大的視窗;發送方的視窗向前滑動,於是它可以多送一些。這個回饋迴路又緊又誠實,因為唯一知道杯子水位的一方——接收方——正是回報它的那一方。完全不需要猜測。這個機制,也就是 TCP 流量控制,在上一段位已經詳細談過;在這裡我們只需要它作為對照,好讓壅塞控制的困難變得清晰可見。

請注意流量控制看不見什麼。接收視窗對中間那條高速公路隻字未提。一個有著慷慨 64 KB 杯子的接收方會樂呵呵地通告一個很大的視窗,而一個只服從流量控制的發送方就會興高采烈地把 64 KB 灌進一條瓶頸只能承載其中極小部分的路徑。流量控制會完全心滿意足,而中間那張網路卻在熔毀。保護接收方與保護網路,根本就是兩件不同的工作。

壅塞控制:去猜一個沒人會告訴你的數字

壅塞控制需要它自己的上限,於是 TCP 為它另外保留了第二個、獨立的額度:壅塞視窗(cwnd)。它是發送方對於「這條網路路徑能容納多少資料而不至於把某台路由器的隊伍塞爆」的私下估計。發送方接著同時服從兩個上限。它被允許在途中持有的量,是這兩個視窗中較小的那一個,我們可以寫成 min(rwnd, cwnd)。流量控制把它限制在接收方的空間,壅塞控制把它限制在網路的空間;較緊的那個獲勝。

Allowed bytes in flight  =  min( rwnd , cwnd )

  rwnd  (receive window)    : the RECEIVER tells you this explicitly
  cwnd  (congestion window)  : the SENDER must GUESS this from feedback

  rwnd small  ->  slow receiver is the limit  (flow control)
  cwnd small  ->  busy network is the limit   (congestion control)
TCP 並排跑著兩個視窗,並受限於較小的那一個。接收視窗是別人告訴你的;壅塞視窗則必須從你封包的遭遇中推斷出來。

這裡有個殘酷的不對稱。rwnd 以一個乾淨的數字出現在每一個確認裡。cwnd 卻沒有這樣的來源——沒有任何路由器會送一個封包說「我已經九成滿了,鬆一點」。(預設情況下沒有;本段位稍後你會遇到 ECN,一種讓路由器明確低聲說出這件事的可選方式,但傳統 TCP 是在沒有它的情況下運作的。)所以發送方必須從它唯一能觀察到的訊號去推斷網路的狀態:哪些封包被確認了、多快被確認、又有哪些彷彿消失了。壅塞控制就是「從間接線索去估計一個看不見的上限」這門技藝。

這個經典的推斷殘酷地簡單:封包遺失意味著網路滿了。它的推理是:在一條有線路徑上,封包消失幾乎唯一的原因,就是某台路由器的隊伍溢出而把它丟掉,所以一次遺失幾乎肯定是壅塞的跡象。當 TCP 偵測到遺失,它就把 cwnd 大幅縮小;當封包持續抵達,它就溫和地把 cwnd 增大,去探測還有沒有更多空間。這個「溫和上升、急遽下降」的模式正是下一篇的核心,在那裡它會變成 AIMD 定律與著名的 TCP 鋸齒波形。

為什麼這很重要:崩潰的威脅

把壅塞控制當成只是個效率上的微調,是很容易的想法。它不是——它是讓網際網路能站著不倒的東西。回想一下,網路沒有一個中央交通警察決定誰能送、能送多少;每一個發送方都自己決定。如果發送方乾脆無視壅塞,把遺失的封包全速重傳進一個早已塞死的瓶頸,真正災難性的事情就可能發生,而歷史上在 1986 年確實發生了:壅塞崩潰

崩潰是一個惡性的回饋迴路。瓶頸超載,於是封包被丟掉。發送方把這個遺失當成「我的封包丟了,我得重送」而重傳——替那條早已過滿的鏈路再添更多流量。現在被丟掉的更多,於是觸發更多重傳。鏈路一直被塞到頂,但穿越它的幾乎全是注定要再被丟一次的重複封包。有用的吞吐量——真正送達的新鮮資料,我們稱之為 有效吞吐量——朝零崩落,即使那條線忙到 100%。網路拼盡全力地運轉,卻幾乎一事無成。

這是本主題至關重要的誠實之處:網際網路之所以不倒,並不是因為有某個權威強制了秩序,而是因為各個端點自願合作。TCP 的壅塞控制就是那份君子協定,每一個行為端正的發送方都會在網路看起來吃緊時退讓。這裡沒有警察——只有一份在數十億台機器上運行的共同紀律。下一篇會講述崩潰的故事,以及把網際網路從中救出來的 AIMD 想法。

本段位其餘篇幅的地圖

既然這兩個問題已經乾淨地分開,以下是前方的路——每一個名字都是我們會真正拆解、而不只是貼標籤的東西。下一篇講述 1986 年崩潰的故事以及解方:AIMD,順利時加一點、出錯時砍很多,這產生了 TCP 招牌的鋸齒波形,也正是讓多條連線公平共用一條鏈路的東西。

  1. 慢啟動與壅塞避免:一條全新的連線如何加速——每一個來回把 cwnd 加倍,好快速找到路徑的容量,接近後再切換成謹慎的線性成長,把鋸齒波形完整地畫出來。
  2. 及時偵測麻煩:估計來回時間以設定一個合理的重傳逾時,以及更聰明的快速重傳與快速恢復,它們對遺失做出反應而不必等一個慢吞吞的計時器響起。
  3. 現代變體:Reno、接著 CUBIC、再接著 BBR 各自如何重新定義「壅塞」究竟是什麼意思——CUBIC 為今日的高速鏈路重塑成長曲線,而 BBR 則完全捨棄遺失,改為直接量測頻寬與延遲。
  4. 來自路由器的協助,以及它的黑暗面:ECN 讓路由器標記一個封包而不是丟掉它、像 RED 與 CoDel 這樣的主動式佇列管理、過大的緩衝區毀掉延遲的緩衝膨脹問題,以及公平究竟意味著什麼。

有一個誠實的警告要帶進接下來的一切。「遺失等於壅塞」這個經典邏輯是一個假設,不是一條定律,而且它有一個著名的失靈模式。在一條無線鏈路上——Wi-Fi、行動網路——封包有時是被無線電干擾、衰落,或一個經過的障礙物給弄壞而遺失的,根本毫無壅塞。基於遺失的 TCP 把這種位元錯誤造成的遺失誤讀成網路滿了,於是毫無必要地猛踩剎車,所以一條完全空閒的無線路徑可能交付遠少於它應有的量。把這個失靈記在心裡;它正是像 BBR 這類變體被設計來填補的那種缺口。

那位謹慎的駕駛

如果你只從這篇帶走一個畫面,就帶這個。TCP 的壅塞控制表現得像一位開在自己無法完全看清的路上、謹慎的駕駛。它溫和地加速——每當一切順利就多踩一點油門——而一旦察覺麻煩就用力剎車,因為對一個真正的堵塞輕輕剎車會釀成追撞。它從不知道真正的速限,也就是那條路徑看不見的容量;它只能一路摸索著靠近它,稍微越過它直到有什麼推回來,然後再鬆開。本段位接下來的全部,就是把這一個本能加以工程化。