下面是 TCP 拥塞状态机:
但它只是冰山一角,这只是 loss-based 状态机,实现一个完全的 delay-based cc 就对不上这个状态机。
该状态机来自 RFC5681,源自 RFC2581,RFC2001,大概在 1990 年代,loss-based 就是范雅各布森(Van Jacobson) 1988 年拥塞控制版本的直接描述。
Linux kernel TCP 在独立的拥塞控制模块之上内置上述状态机,这意味着 Linux kernel TCP vegas 等 delay-based cc 并不完备,依然无条件以 reno 方式响应丢包。
幸运的是,BBR 后 Linux TCP cc 框架导出了 cong_control 回调足以绕开该 loss-based 拥塞状态机,实现 cong_control 回调重写 vegas 即可实现完整的 delay-based cc。
重传和 loss 紧密关联,绕开该拥塞状态机是解除重传和 cc 之间的耦合的关键。随着 TCP 拥塞控制的演化迭代,无论标准上还是在实现上,解耦重传和 cc 的过程一直在进行。
简单描述这过程。
如拥塞状态机所示重传和 cc 之间的胶合主要在于传输状态如何影响 cwnd,背后是不同兑换比的数据包守恒:
- 兑换比大于 1:Open 状态(如拥塞避免的 additive increase,BBR probe)
- 兑换比等于 1:收到几个 (S)ACK 发出几个 Data,如 BBR 丢包状态
- 兑换比小于 1:Recovery 状态(如快速重传的 PRR multiplicative decrease)
理论上,cc 只管 when & how much,重传只管 what,二者无关联,但内置拥塞状态机实现让二者不得不胶合。
一旦检测到丢包,拥塞状态机转换,cwnd 变小,即使确认非拥塞丢包,依然要忍受小 cwnd,更严重的是,一旦 cwnd 变小,rate-based cc 几乎无法运行,因为没有足够 data 支撑 delivery rate 测量。
由于 loss-based 拥塞状态机的假设是拥塞引发丢包,在 Recovery 状态无条件谨慎,导致丢包重传状态更容易跌入兜底 Loss 状态。
为避免过多跌入 Loss 状态,TCP 标准沿以下路径迭代:
- reno:一次多个丢包很容易导致 ACK 时钟丢失,跌入 Loss。
- SACK:解决丢包检测问题,可重传多个包,但小于 reordering 个 SACK 不会触发重传,且每个 Lost 包只能重传一次,不然还会跌入 Loss。
- FACK:不再依赖 reordering 个 SACK,取 seq 序,但在 Recovery 依然只有一次重传机会。
- RACK:不再取 seq 序,改取时间序,可为每个报文登记 xmit time,在 Recovery 重传次数不再受限。
- Next?我觉得 RACK 可以覆盖 Loss 了,TCP 甚至不再需要为 timeout 保留单独的 Loss 状态。
在实现上,TCP 也逐步迭代,导出 cong_control 回调解放了 cc,可在 Recovery 状态自行管理 cwnd。
TCP 标准不断为 Recovery 状态准备更多可供发送的数据,TCP 实现也逐步摆脱 loss-based 假设。如果看 Linux kernel TCP 的实现,6.1 的 TCP 实现比 3.10 更简单,印证如上所述。与此前的 TCP 相比,更新的实现:
- scoreboard 管理更简单,废除 forward-retrans。
- seq reordering 被 RACK 废除,内置于时间序。
是不是精简了很多。
总体来讲,“为 Recovery 状态准备更多可供发送的数据” 这件事除了标准演进之外,各种增补优化也一直在努力,如典型的 early retrans 为 thin stream 放宽了 fast retransmit 触发条件,而 tail loss probe(TLP) 则在尾部增加触发 fast retransmit 的弹药(这会儿没新数据可供传输,片刻后就有了呢,尽量用新数据触发 SACK 信息的带回)。
漫长的一周终于结束,聊一下 RFC5681 TCP 拥塞状态机的演进。
浙江温州皮鞋湿,下雨进水不会胖。