兩件看起來很像、其實不同的工作
在上一篇裡,我們在兩件工作之間劃了一條清楚的界線——它們最後都歸結到「讓傳送端慢下來」,卻保護著截然不同的東西。流量控制保護的是接收端:它阻止一個快速的傳送端把一個慢吞吞接收者的緩衝區灌爆,靠的是接收端在每一個確認裡通告的接收視窗。而壅塞控制(congestion control)保護的是網路本身:它阻止所有傳送端聯手把中間的路由器灌爆——那裡的緩衝區會被填滿,封包會開始被丟棄。接收端可能還空著好幾 GB,網路卻已經噎住了——這兩道上限是各自獨立的。
難就難在,沒有任何單一傳送端看得見整張網路。沒有交通警察,沒有一塊顯示路由器有多滿的中央儀表板。一台在台北的筆電上的傳送端,根本不曉得另外有一萬個傳送端,正灌進法蘭克福同一條已經超載的鏈路。所以 TCP 得做一件近乎魔法的事:只憑著從遠端傳回來、微弱的線索,去推斷一項它無法直接觀察的共享資源的狀態。壅塞控制聰明之處,幾乎全在於如何把那些線索讀對。
網際網路差點卡死的那一天
要看清楚我們究竟為什麼需要壅塞控制,不妨想像把它整個關掉的網路。每個傳送端就只是用自己鏈路允許的最快速度狂噴封包,丟掉什麼就重送什麼。現在來讓一台路由器超載:封包灌進來的速度,快過它外送鏈路能排空的速度,緩衝區被填滿,於是路由器做了它唯一能做的事——把滿出來的丟掉。每個被丟掉的封包,最終會在它的傳送端逾時,於是傳送端盡責地把它重送。但鏈路早就飽和了,所以這次重送只是再添負載,造成更多丟棄、又造成更多重送。大家愈用力推,真正能送達的反而愈少。
這個惡性螺旋有個名字:壅塞崩潰。它指的是這樣一種狀態——網路忙得不可開交、全速地在搬運封包,但有用的有效傳輸量(goodput),也就是真正抵達且被接收端接受的位元組(不是重複封包、也不是註定白費的重送),卻一路崩跌到趨近於零。這不是思想實驗。1986 年 10 月,早期網際網路上兩個相距僅幾百公尺的站點之間,就正是這樣崩潰的:傳輸量掉了大約一千倍,從 32 kbps 直落到 40 bps。正是那場驚嚇,催生了 Van Jacobson 的壅塞控制演算法——本級接下來一切的祖先。
透過丟包來讀懂網路的心思
如果傳送端看不見路由器,那它能讀的線索是什麼?Jacobson 的洞見漂亮地走了一條間接的路:在一條健康的有線鏈路上,封包幾乎不會因為隨機因素而遺失。所以當一個封包真的不見了,壓倒性最可能的原因,就是某台路由器的緩衝區滿溢了——換句話說,網路壅塞了。因此 TCP 把封包遺失當成網路在說「慢下來」的方式。丟包就是那個隱含的回饋訊號,即使沒有任何路由器真的送出一則明確的訊息。
TCP 用兩種方式偵測這種遺失,接下來兩篇會完整展開。比較粗的是基於來回時間的計時器:如果某個區段的確認在預期時間內沒到,就假設它丟了。比較銳利的是重複確認——當接收端不斷重新確認它手上「依序」收到的最後一個位元組時,那種重複就是一個暗示:有單獨一個封包不見了,但後面的封包卻到了。無論哪一種,傳送端都會推斷出壅塞並做出反應。對本篇而言,偵測機制不如那個反應重要——而那個反應,是由一條優雅的控制法則所掌管的。
壅塞視窗:一條自我設下的速限
傳送端究竟是怎麼「慢下來」的?它會保管一個私有的數字,叫做壅塞視窗,通常寫成 cwnd,以位元組計(或者,為了直覺,以封包計)。規則很簡單:傳送端在任何時刻,最多只能有 cwnd 個位元組的資料「在途中」——也就是已送出但尚未被確認。cwnd 小,就是涓涓細流;cwnd 大,就是滔滔洪水。關鍵是,接收端從不會看到 cwnd、也從不會設定它;它是傳送端自己給自己設下的天花板,是它對「網路目前吃得下多少」所做的猜測。
別忘了,流量控制早就給了傳送端一道天花板:接收視窗 rwnd。所以傳送端在每一個瞬間,都遵守兩者中較小的那一個:它被允許「在途中」的量,是 min(cwnd, rwnd)。接收視窗保護接收者,壅塞視窗保護網路,傳送端遵循的,是當下較緊的那一道。整門壅塞控制的藝術,最後都化約成一個問題:傳送端只知道封包何時送達、何時遺失,那它該如何隨時間調高、調低 cwnd?
AIMD:緩緩加速、用力煞車
答案正是本篇的核心:AIMD,是「加法增大、乘法減小(Additive Increase, Multiplicative Decrease)」的縮寫。這條規則有兩面,而且刻意地不對稱。當封包持續送達時,傳送端把 cwnd 調高一個小而固定的量——大約是每來回一趟加一個封包的份量。這就是加法增大:一種緩慢、線性、有禮貌的向上試探,問著「網路還能再吃一點嗎?」但是它一看到丟包,就不是把 cwnd 輕推一下而已——它會把它砍掉,通常砍掉一半。這就是乘法減小:一次突然而劇烈的後撤。
把 AIMD 想成一位在霧中道路上小心翼翼的駕駛。他輕柔地加速,每一秒快一點點,去摸索安全邊界在哪。一旦感覺到危險——也就是丟包——他就用力煞車,把速度砍半,然後又從頭開始那段輕柔的加速。把 cwnd 對時間畫出來,你就會得到那著名的 TCP 鋸齒波(sawtooth):緩緩的線性爬升、一道陡直的懸崖落下,爬升、墜崖、爬升、墜崖,永無止盡。這種無休止的試探不是缺陷;它正是 TCP 用來不斷重新量測一張網路的方式——這張網路的容量,會隨著其他資料流來來去去,每一秒都在變。
cwnd over time under AIMD (each '/' = additive increase per RTT, each vertical drop = halving on a detected loss): cwnd | /| /| /| | / | / | / | | / | / | / | | / v / v / v | / | / | / | |/ |/ |/ | +----------------------------------------> time additive increase ^ loss: cut cwnd in half (multiplicative decrease)
為什麼要不對稱——又為什麼這樣才公平
為什麼加得慢、卻砍得狠?因為這兩種錯誤的危險程度並不相等。衝過容量上限,會重新點燃我們先前看到的崩潰螺旋,所以安全的做法,是一見麻煩苗頭就快速而果斷地後撤。而沒衝到上限,頂多浪費一點頻寬,而那一點,溫和的加法爬升會悄悄地補回來。AIMD 的設計就是要「失誤時也安全」:拿不準的時候,給網路一點喘息的空間。這種不對稱才是重點所在,不是調參數時的偶然產物。
這裡還有一個更深的回報,而且近乎令人意外:AIMD 是公平的。想像兩條資料流共用同一條瓶頸鏈路,一開始的速率天差地遠。每一輪,兩者都加上同樣固定的量,而在一次共同的丟包上,兩者都砍掉同樣的比例。加上相等的量,不會改變它們之間的差距;但砍掉相同的比例,每一次都會縮小那個差距——較大的那條流,絕對量上會損失得更多。把這件事往前推演,兩者的速率會自己漂向相等,不需要協商、不需要中央裁判。這種自我組織出來的公平,正是TCP 公平性的意思,而 AIMD 幾乎是免費地把它交付出來。
有兩點誠實的但書,讓這幅圖像保持貼近現實。第一,AIMD 只在「共用同一瓶頸、且來回時間相近」的資料流之間收斂到公平;一條來回時間短得多的流會爬得更快、搶到更大的份額,所以「公平」是近似的,不是精確的。第二,這裡描述的 AIMD 是穩態行為,是壅塞控制的「巡航」狀態。它不是一條連線「啟動」的方式——一條全新的連線根本不曉得這鏈路吃得下多少,所以它會用一段更快的爬升,叫做慢啟動,而那正是下一篇要接著講的故事。