流量控制 — TCP 的滑動視窗
接收端在每次 ACK 時通告「還能再收多少位元組」,送信端把等待 ACK 的位元組數收在這個額度內。這扇窗的滑動決定了吞吐量的上限。
快的送信端,會把慢的接收端淹死
就算重傳的機制齊全了,承諾仍未完成。如果送信端的伺服器高性能,而接收端是非力的行動裝置呢?送達的資料會先放在接收端的接收緩衝區,等待應用程式以 recv() 讀走。應用程式的讀取若跟不上傳送的步調,緩衝區就會溢出,好不容易送達的資料被丟棄、還得請對方重傳。這就本末倒置了。
於是 TCP 加入了承諾之 3 的前半 — 流量控制。機制是共乘在第 3 章的 ACK 上。接收端每次回 ACK 時,都一併申告「從已確認的位置起,再這麼多我還收得下」的剩餘容量 — 視窗(rwnd)。送信端只在已送出但 ACK 尚未回來的位元組數(in-flight)不超過這個額度的範圍內繼續送。
從已確認的端點(ack = 20001)起視窗 5000 位元組份是「可以送的範圍」。ACK 前進時整扇窗向右滑,所以稱為滑動(sliding)視窗。
把位元組序列塗成 4 種顏色
從送信端來看,位元組的序列任何時候都能塗分成 4 個區間。以 ack = 20001、視窗 5000、已送信到位元組 23000 為止的例子來看。
視窗也可能被通告為 0(零視窗)。這是接收應用程式沒有把緩衝區讀走的狀態,送信端停止傳送,用視窗探測這種小型詢問偶爾打聽狀況並等待。封包擷取裡如果排著零視窗,該懷疑的不是網路,而是接收端應用程式的處理堵塞。
練習 4-1 — 手算視窗的兩端
把「可以送到哪裡」的界線,用位元組的編號準確地畫出來。
Q1. 接收端搭載在 ACK 上通告的視窗(rwnd)值所代表的東西是哪一個?
rwnd 是接收緩衝區的剩餘容量。接收端抱著「已送達但應用程式還沒讀走的資料」,並在每次 ACK 時申告剩餘容量。送信端讓等待 ACK(in-flight)的位元組數不超過這個額度地傳送。頻寬與路徑的壅塞不會出現在 rwnd 上 — 處理那邊狀況的是第 5 章的壅塞控制。
Q2. 送信端收到了 ack = 20001(20000 為止已確認)、通告視窗 5000 位元組的 ACK。目前已送信到位元組 23000 為止。現在可以追加傳送的是多少位元組?
可以送的右端是「已確認的端點 + 視窗」= 20000 + 5000 = 25000。已送信到 23000 為止,所以 in-flight 是 20001〜23000 的 3000 位元組。剩餘額度是 25000 − 23000 = 2000 位元組。每當 ACK 前進(左端向右移動),右端也跟著滑動前進 — 這個動作就是滑動視窗(sliding window)名稱的由來。
Q3. 接收端通告了視窗 0(零視窗)。送信端正確的行為是哪一個?
零視窗是「接收緩衝區已滿 — 應用程式把資料讀走之前,1 個位元組都收不了」的申告,所以送信端等待才是正解。但若只是默默等待,當「視窗打開了」的通知遺失時就會變成永遠的等待,因此用視窗探測定期詢問狀況。故障調查時的要害在於:零視窗的主犯不是網路,而是接收端應用程式讀取太慢(第 7 章)。
窗的大小決定速度的上限 — 視窗 ÷ RTT
流量控制有一個重要的副作用。送信端把等待 ACK 的量保持在視窗以下,所以 1 個 RTT 之間能送的最多就是一整個視窗。也就是說,不管頻寬多粗,都無法超過
吞吐量上限 = 視窗 ÷ RTT
。視窗 100 KB、RTT 100 毫秒的話,100,000 位元組 ÷ 0.1 秒 = 1 MB/秒就是天花板。要毫無空隙地用滿線路,需要頻寬 × RTT(頻寬延遲乘積,BDP)以上的視窗。「從附近的伺服器很快,只有從海外的伺服器慢」的情況,多半能用這條式子說明。
模擬器的動作概要(文字版):拖動 RTT、視窗、頻寬 3 個滑桿,1 個 RTT 能送的量(= 視窗)、由視窗 ÷ RTT 決定的吞吐量上限、頻寬 × RTT(BDP)、實效吞吐量(視窗受限與頻寬受限中較小的一方)會以數值更新。同時,以橫條顯示 in-flight 的區段在路徑這條水管中塞得多滿(填充率與飛行中區段數的概估),並以文字說明現在決定上限的是視窗還是頻寬。
希望你在模擬器中嘗試的 3 個實驗
- 只把 RTT 調成 10 倍 — 視窗與頻寬保持不變。請確認上限掉到 10 分之 1、水管變得空蕩蕩。
- 把視窗對齊 BDP — 找出填充率剛好變成 100%、決定上限的因素從「視窗」切換成「頻寬」的分界。應該會和練習 4-2 第 3 題的答案是同一個值。
- 把視窗調得比 BDP 大 — 上限不會再增加了。請確認就算把窗開得再大,也流不了比水管更粗的水。
練習 4-2 — 視窗與 RTT 決定吞吐量
把模擬器的計算式也用自己的手確認一次。設 1 MB = 100 萬位元組。
Q4. 視窗 100 KB(100,000 位元組)、RTT 100 毫秒的連線所能達到的吞吐量上限是多少 MB/秒?(1 MB = 100 萬位元組)
送信端在 ACK 回來之前最多只能送出視窗份(100,000 位元組),而 ACK 是 1 個 RTT(0.1 秒)後回來。也就是每 1 RTT 最多 1 個視窗,100,000 位元組 ÷ 0.1 秒 = 1,000,000 位元組/秒 = 1 MB/秒。吞吐量上限 = 視窗 ÷ RTT — 這是本課程中在實務上最常用的式子。
Q5. 明明是頻寬 1 Gbps 的高速線路,卻只有遠距離(RTT 大)的傳輸特別慢,這種事會發生。正確的理由是哪一個?
送出一整個視窗、等 ACK 往返一次,如此反覆,所以上限是視窗 ÷ RTT。RTT 5 毫秒時綽綽有餘的視窗,在 RTT 150 毫秒時上限會變成 30 分之 1。線路再怎麼粗,窗太小水就流不動。對策是加大視窗(近年的 OS 會用視窗縮放自動調整),並不會發生協定切換這種事。
Q6. 要把頻寬 10 MB/秒、RTT 100 毫秒的路徑毫無空隙地用滿,最少需要多少 MB 的視窗?(計算頻寬 × RTT。1 MB = 100 萬位元組)
填滿路徑所需的 in-flight 量是頻寬 × RTT(頻寬延遲乘積,BDP)。10,000,000 位元組/秒 × 0.1 秒 = 1,000,000 位元組 = 1 MB。視窗在 BDP 以上時上限由頻寬決定,不足 BDP 時上限就是視窗 ÷ RTT。請在模擬器中確認「視窗受限」與「頻寬受限」的分界正好就是這個值。
本章帶走的重點
- 流量控制是配合接收端狀況的機制。rwnd 是接收緩衝區空位的申告
- 可以送的右端 = 已確認的端點 + 視窗。讓 in-flight 不超過視窗地傳送
- 吞吐量上限 = 視窗 ÷ RTT。要用滿就需要視窗 ≥ 頻寬 × RTT(BDP)
- 零視窗的主犯是接收端應用程式的讀取堵塞。懷疑網路之前先看應用程式
不過,光配合對方的狀況還不夠。下一章是承諾之 3 的後半 — 配合途中網路狀況的壅塞控制。另一扇窗 cwnd 將要登場。