參考前面的文章 一個原教旨的多路徑 TCP 和 MP-BBR 公平性推演,一直都破而不立,不能光說怎樣不好,還得說說現狀情況下,該如何是好。
如果 receiver 亂序重排的能力有限(拜 TCP 所賜),如果非要在多路徑上傳輸 TCP,如果不考慮公平性,單單如何提高吞吐確實難,要找出來原因, 才能徹底解決它,得先分析問題。
設一條 TCP 連接存在兩條子路徑(subflow):
- 快路徑(Path1):帶寬 B1,單向時延 D1;
- 慢路徑(Path2):帶寬 B2,單向時延 D2,且 B1 ≥ B2, D1 ≤ D2。
發送策略:
- 固定間隔 Round-Robin:每間隔 T 時間,輪流在 Path1 和 Path2 上各發送 1 個包;
- 單快路徑(Path1 Only):所有數據僅通過 Path1 發送,間隔 T 時間發送 1 個包。
假設:
- 所有數據包大小均為 L 字節;
- 接收端需按序重組數據,亂序包會導致阻塞。
分析 Round-Robin 吞吐量:
- 發送速率
- Round-Robin 策略下:
- 每 2T 時間發送 2 個包,Path1 和 Path2 各 1 個;
- 平均發送速率: 2 L 2 T = L T \dfrac{2L}{2T}=\dfrac{L}{T} 2T2L?=TL?。
- 路徑帶寬限制
- Path1 的帶寬 B1 滿足: L T ≤ B 1 \dfrac{L}{T}\le B_1 TL?≤B1?;
- Path2 的帶寬 B2 需滿足: L T ≤ B 2 \dfrac{L}{T}\le B_2 TL?≤B2?;
- 由于 B1 ≥ B2,若 L T > B 2 \dfrac{L}{T}\gt B_2 TL?>B2?,Path2 會丟包,因此最大穩定發送速率需滿足: L T ≤ B 2 \dfrac{L}{T}\le B_2 TL?≤B2? ? T ≥ L B 2 T\ge\dfrac{L}{B_2} T≥B2?L?,即 T 不能太小,否則慢路徑無法及時傳輸。
- Round-Robin 策略下:
- 接收端亂序阻塞
- 數據包到達時間
- Path1 的包到達時間:D1 + k?2T,k 為輪次;
- Path2 的包到達時間:D2+k?2T
- 亂序條件
- 若 D2 + k?2T > D1 + (k + 1)?2T,即 D2 ? D1 > 2T,則 Path2 會阻塞 Path1,快路徑要等慢路徑。
- 有效吞吐量
- 由于亂序阻塞,系統吞吐量受限于慢路徑的到達速率,慢路徑形成漏桶短板;
- 每 2T 時間可交付 2 個包,但實際受 D2 限制,因此 G o o d p u t R R = 2 L 2 T + m a x ( 0 , D 2 ? D 1 ? 2 T ) \mathrm{Goodput}_{RR}=\dfrac{2L}{2T+\mathrm{max}(0,D_2-D_1-2T)} GoodputRR?=2T+max(0,D2??D1??2T)2L?,若 D2?D1>2T,則吞吐量進一步下降。
- 數據包到達時間
再分析單快路徑的吞吐量:
- 發送速率:
- 每 T 時間發送 1 個包,速率 L T ≤ B 1 \dfrac{L}{T}\le B_1 TL?≤B1?。
- 無亂序問題,所有包按順序到達,接收端無需等待。
- 有效吞吐量: G o o d p u t S i n g l e = L T \mathrm{Goodput}_{Single}=\dfrac{L}{T} GoodputSingle?=TL?
效率對比的結論非常明確, G o o d p u t S i n g l e ≥ G o o d p u t R R \mathrm{Goodput}_{Single}\ge\mathrm{Goodput_{RR}} GoodputSingle?≥GoodputRR?,當且僅當 Path1 與 Path2 效率相同時取等號,兩條路徑差異越大,吞吐下降越明顯,這就是拖后腿。
力氣小的經理和力氣大的工人一起扛磚頭,不如讓力氣大的自己去扛,兩人的協調性成本太大,正與 TCP 亂序重組協調性成本太大一致。怎么辦?好辦!
讓力氣小的經理自己去搬他自己的磚,不要和力氣大的工人協調。換到 TCP,還是老方法論,橫豎一顛倒,轉換一個視角,將空洞留在 sender 而不是 receiver。
怕在 receiver 處快路徑等慢路徑,那就慢路徑笨鳥先飛,先到了等快路徑即可,落實到具體操作也簡單:
- 在 sender 處,快路徑從發送隊列頭部正序發送,慢路徑從發送隊列尾部反序發送(最后我再解釋為什么),它們處理各自的丟包重傳,慢路徑直到和快路徑 gap 小于閾值 K = f ( R T T ) K=f(\mathrm{RTT}) K=f(RTT)(亦可為一個經驗值常量) 后停止發送,推動下一個窗口。
這很好解釋,協調性不一致會付出成本時,解除協調性耦合即可,耦合就會導致木桶效應,各干各的更合適。
直觀上理解,如果我們有 4GB 的文件,一快一慢兩條路徑,快路徑依次發送 0,1,2,3,…,慢路徑依次發送 4G - 1,4G - 2,4G - 3,…,假設它們在 X 字節處相遇,設快路徑平均吞吐為 B,總傳輸時間將會從 4 G B B \dfrac{4GB}{B} B4GB?下降到 X B \dfrac{X}{B} BX?。
至于 receiver 緩沖區問題,那不是問題,由于已經完全解耦了兩個路徑的傳輸,也就成了兩個獨立的流,各自維護接收緩沖區,最后將它們拼接在一起也行。
另一個問題,看起來以上策略僅對下載大塊數據有效,對強調時延穩定性的業務不好使,其實也未必,問題的關鍵在于業務能容忍多大的 buffer 時延,只要該 buffer 大于多路徑最大 BDP,就能應用此方案,只是把上面的 4GB 換成了該 BDP + a。
總之,總要拿個什么來交換成本,以獲得收益,越復雜越不劃算。舉個例子,對于獲得肉類的需求而言,馴養食肉動物的轉化率遠沒有食草動物高,因為食肉動物處在食物鏈更上的生態位,它更復雜,也更不劃算,復雜性本身就是成本的組分。
Linux Kernel 自帶的 MPTCP 調度算法跟我這個看起來類似,但它沒有反著發,而是再慢路徑直接發送 s e q = S E Q c u r r + α ? B D P f , 其中 α = R T T s R T T f \mathrm{seq}=\mathrm{SEQ_{curr}}+\alpha\cdot\mathrm{BDP}_f,其中 \alpha=\dfrac{\mathrm{RTT}_{s}}{\mathrm{RTT}_{f}} seq=SEQcurr?+α?BDPf?,其中α=RTTf?RTTs??,思路依然是讓慢路徑序列號往后錯,但它仍傾向于精確拼接,企圖兩邊正好配合,但這是不可能的,慢路徑非常容易與快路徑耦合,一旦被快路徑趕上,Seq 在快慢路徑交錯傳輸和重傳,就會引發持續性 HoL 抖動,而這個錯開量 α ? B D P f \alpha\cdot\mathrm{BDP}_f α?BDPf? 由于 RTT 和吞吐率本身的測量誤差幾乎不可能太準確,也就難怪效果不好了。
之所以倒著發,為了保證慢路徑后段一定與快路徑解耦(確保后邊的數據已經完全準備好),即使 gap 算大了,僅靠快路徑自身也能很快接上慢路徑已經準備好的數據,而如果 gap 算小了,則影響更小,換句話說,即使抖動也是一瞬間,且快路徑主導補洞。
當然,TCP 本身并不支持倒序發送,那這個就不是 TCP 了。既然不是 TCP,索性就多做一點,連 RTT 測量問題也給解了。學學 Swift,到處打標時間戳就很高尚。
其實要更精確地測量兩條路徑的 RTT 差異非常容易,無論哪條路徑收到一份數據需要確認時,將確認報文同時發到多條路徑上,sender 只需要比較兩個確認報文的時間差,并乘以快路徑的吞吐即可獲得偏移量,兩個問題遺留:
- 這可能只是反向單向時間戳之差,而正向時間戳更需要,讓 receiver 采集數據包時間戳信息并回送回來;
- 確認報文雙發會不好浪費帶寬,會,但并不嚴重,且這是小包加速的典型套路。
既然重新做協議,不要盡抄 TCP 就是了,但要盡量保持簡單。
浙江溫州皮鞋濕,下雨進水不會胖。