只靠遺失當訊號的麻煩
到現在你已經懂了經典 TCP 的核心:它溫和地加速、猛烈地煞車,那條畫出 壅塞控制鋸齒的 AIMD 法則。但退一步問問:發送方究竟是怎麼得知自己推得太用力了?在樸素的 Reno 式 TCP 裡,唯一的訊號就是一個被丟掉的封包。路徑上某處的路由器塞滿了它的佇列、空間用盡,於是把一個封包扔掉;那則缺席的確認,就是告訴發送方該退讓的東西。換句話說,網路說「慢下來」的方式,是先把你的資料毀掉,再讓你發現那個缺口。
這是一場相當粗糙的對話。發送方得先損失資料,再等上至少一個來回時間才能察覺。而它引出一個問題,這問題其實是本篇一切的核心:那個遺失究竟發生在哪裡?封包在被丟棄或被遞送之前,又等了多久?答案就藏在路由器的緩衝區裡——那條在繁忙鏈路上等著輪到自己的封包佇列。讀懂這個緩衝區,就是同時讀懂緩衝膨脹、ECN 與公平性的鑰匙。
緩衝膨脹:更大的緩衝區反而更糟
這是現代網路裡最反直覺的結果之一。路由器需要一個緩衝區,好讓一陣短暫的封包爆量不至於在鏈路一忙起來的瞬間就被丟掉——這確實有用。於是某位工程師推論:記憶體很便宜,讓我給它一個超大的緩衝區,這樣它幾乎永遠不必丟棄任何東西。結果是一場名叫 緩衝膨脹 的災難:鏈路感覺起來更慢、更卡,而非更快,即使根本沒有任何封包遺失。要看出為什麼,記住:基於遺失的 TCP 會一直推得更用力,直到看見一次丟棄。如果緩衝區極其龐大,發送方就會在任何丟棄發生之前,把它填到滿溢。
一個塞滿的緩衝區,意味著每一個新封包都必須排在所有已在佇列裡的封包後面。鏈路每秒仍搬動同樣多的位元——你的頻寬沒變——但現在每個封包離開前都得在一條長隊裡等。那段等待是純粹的延遲,疊加在無可避免的傳播延遲之上。此刻正好回想階梯早期那個誠實的事實:頻寬、吞吐量與延遲是三件不同的事,而一個肥大的緩衝區能在頻寬看起來完美無瑕的同時,悄悄毀掉延遲。你的大型下載順利完成;與此同時你的視訊通話卻卡頓、你的鍵盤輸入也延遲,因為每個封包都被卡在那筆大宗傳輸後面,排在一條可能裝著好幾秒流量的佇列裡。
更聰明的路由器:AQM 與一個位元的警告
如果路由器只在緩衝區完全塞滿時才丟棄,那它早已任由佇列變得又長又卡之後,才開口說話。療法是讓路由器刻意地幫忙,而非被動地等到溢出。這一族技術叫做 主動佇列管理,簡稱 AQM。想法是:盯著佇列,當它開始堆積時——遠在溢出之前——就採取一個小動作,推促發送方慢下來,把佇列保持得短、把延遲保持得低。
經典的 AQM 方案是 RED(隨機早期偵測):當平均佇列長度漲過一個門檻,路由器就開始提早丟棄一小撮隨機的封包。每一次提早丟棄,都是給某條流的一個禮貌暗示,要它退讓,並公平地分攤到送得最多的那些人身上,遠在緩衝區真正塞滿之前。一個更現代、會自我調校的方案是 CoDel(受控延遲),它的巧妙之處在於:它盯的不是佇列裡排了幾個封包,而是它們已經等了「多久」。CoDel 量測每個封包待在佇列裡的時間,並在那段延遲持續偏高太久時才動作——直接從根源攻擊緩衝膨脹問題,因為我們真正在意的是延遲,而非佔用量。
但注意:即使是 RED 與 CoDel,仍是靠丟棄一個封包來發出警告——毀掉資料來表態。優雅的最後一步,是在「不扔掉任何東西」的前提下警告發送方。那就是 顯式壅塞通知,簡稱 ECN。當佇列堆積時,具備 ECN 能力的路由器不丟封包,而是翻動封包 IP 標頭裡單獨一個標記位元,表示「我快要壅塞了」。接收方看到這個標記,在它的確認裡回送給發送方,發送方就把這個標記當成「彷彿它看見了一次遺失」來處理——它退讓——但從沒有任何資料遺失,也不必重傳任何東西。一個一位元的「請放鬆一點」,在不弄壞任何東西的情況下送達。
WITHOUT ECN (loss as signal) WITH ECN (mark as signal) --------------------------- -------------------------- queue fills -> router DROPS packet queue builds -> router MARKS packet sender notices missing ACK receiver echoes the mark in its ACK sender backs off (AIMD) sender backs off (AIMD) ...and must RETRANSMIT lost data ...nothing lost, nothing resent
Reno、CUBIC、BBR:對壅塞的不同理論
發送方放大與縮小其 壅塞視窗的策略,並非刻在石頭上——它是一個可替換的演算法,而演算法的選擇,編碼了一整套關於「壅塞究竟是什麼」的理論。Reno,你已研究過的那個經典,相信遺失等於壅塞:線性成長,遇損失就減半。它管用,但在今日極快、長距離的鏈路上,它謹慎的線性攀升要花上好久才能在每次切半後重新填滿管道,讓容量閒置。
CUBIC,多年來 Linux 與 Android 的預設,保留了「遺失等於壅塞」的信念,但讓視窗沿著一條更平滑的三次曲線成長。在一次切半之後,它迅速爬回上次管用的速率附近,然後在接近那個點時謹慎地放慢,溫和地探測是否還有更多空間。這比 Reno 那條直線把又快又肥的鏈路填得好得多,這也正是它成為現代網際網路主力的原因。
BBR 採取了一個真正不同的立場,而這正是整篇導讀重要的原因。BBR 拒絕等待遺失。它反而主動量測兩件事——鏈路可用的頻寬,以及連線的最小來回時間——並試圖恰好以瓶頸速率發送,同時讓佇列幾乎保持淨空。實際上,BBR 是想從一開始就避免製造緩衝膨脹:它瞄準的是「頻寬全滿、卻沒有常駐佇列」的甜蜜點,而不是把緩衝區填滿到有東西壞掉為止。它不是魔法,對鄰居也不見得總是更公平,但它證明了:遺失只是一種訊號的選擇,並非唯一可能的那一種。
公平性,以及無線的陷阱
現在來到那個深刻的問題。沒有人掌管網際網路的流量;沒有一個中央調度者替每條流分配一塊共享鏈路。那為什麼擁擠的鏈路不會乾脆崩塌成「一條貪婪的流獨佔一切」?答案是 公平性,而它是 AIMD 本身一個美麗的湧現性質。當數條 AIMD 流共享一個瓶頸時,它們全都加法式地增加、又全都在大致相同的時刻乘法式地切減,久而久之它們的速率會漂向相等的份額——不是因為有誰強制執行,而是因為「加法增加、乘法減少」這條規則,自然會把不相等的流拉回彼此身邊。公平性內建在「大家如何禮貌退讓」的數學裡。
但誠實要求我們講清楚但書。這種公平性假設大家都跑著大致相同的 AIMD 式演算法、並共享相近的來回時間——而這兩點都不總是成立。一條來回時間較短的流,反應更快、成長更快,所以它傾向在同一個瓶頸搶到比長距離流更大的份額。而發送方大可跑一個更激進的演算法,或一次開很多條連線,來霸佔超過它「應得」的那一塊。公平性是參與者合作時湧現的一種「趨勢」,不是網路強加的「保證」。這正是你很久以前見過的同一個端到端原則:網路在中間保持簡單而笨拙,良好行為住在邊緣的主機裡——這很強大,但只在主機願意配合的程度上才可靠。
這帶出整級最重要的誠實但書,那個藏在所有基於遺失的 TCP 底下的假設。Reno 與 CUBIC 相信遺失意味著壅塞。在有線鏈路上這幾乎總是真的——封包之所以遺失,是因為某個佇列溢出了。但在無線鏈路上,封包經常因為無線電干擾、衰落與多重路徑而遺失,根本沒有任何壅塞。基於遺失的 TCP 分不出這兩者:它看見被丟掉的封包,便假設網路壅塞了,於是把視窗切成一半——在鏈路其實好端端、根本沒有壅塞可紓解時,硬是把自己掐住。那個誤判正是 TCP 在無線上表現不佳的核心,也是「像 BBR 這種不把遺失等同於壅塞的、基於量測的設計」在今日壓倒性以無線為邊緣的世界裡如此重要的一大原因。
把整級串起來
退一步,看清這一級的弧線。你把 流量控制(保護單一個緩慢的接收方)與壅塞控制(保護共享的網路)區分開來。你看到了為什麼壅塞崩潰能把一個網路砸出大坑,以及 AIMD「溫和上、猛烈下」的節奏如何既避開它、又幾乎像附帶福利般公平地分享鏈路。你走過了慢啟動與壅塞避免的鋸齒,遇見了逾時與快速重傳,並看著演算法從 Reno 演進到 CUBIC、再到 BBR。這最後一篇補上了畫面的另一半:網路並不只是一根發送方盲目探測的笨水管。路由器能夠幫忙——用 AQM 把緩衝區保持得短,並用單獨一個 ECN 位元、而非靠毀掉資料來警告發送方。