参考前面的文章 一个原教旨的多路径 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≥B2L,即 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,其中α=RTTfRTTs,思路依然是让慢路径序列号往后错,但它仍倾向于精确拼接,企图两边正好配合,但这是不可能的,慢路径非常容易与快路径耦合,一旦被快路径赶上,Seq 在快慢路径交错传输和重传,就会引发持续性 HoL 抖动,而这个错开量 α ⋅ B D P f \alpha\cdot\mathrm{BDP}_f α⋅BDPf 由于 RTT 和吞吐率本身的测量误差几乎不可能太准确,也就难怪效果不好了。
之所以倒着发,为了保证慢路径后段一定与快路径解耦(确保后边的数据已经完全准备好),即使 gap 算大了,仅靠快路径自身也能很快接上慢路径已经准备好的数据,而如果 gap 算小了,则影响更小,换句话说,即使抖动也是一瞬间,且快路径主导补洞。
当然,TCP 本身并不支持倒序发送,那这个就不是 TCP 了。既然不是 TCP,索性就多做一点,连 RTT 测量问题也给解了。学学 Swift,到处打标时间戳就很高尚。
其实要更精确地测量两条路径的 RTT 差异非常容易,无论哪条路径收到一份数据需要确认时,将确认报文同时发到多条路径上,sender 只需要比较两个确认报文的时间差,并乘以快路径的吞吐即可获得偏移量,两个问题遗留:
- 这可能只是反向单向时间戳之差,而正向时间戳更需要,让 receiver 采集数据包时间戳信息并回送回来;
- 确认报文双发会不好浪费带宽,会,但并不严重,且这是小包加速的典型套路。
既然重新做协议,不要尽抄 TCP 就是了,但要尽量保持简单。
浙江温州皮鞋湿,下雨进水不会胖。