3. 经典拥塞控制算法:慢启动、拥塞避免、快速重传、快速恢复

TCP 的拥塞控制,说白了就是一套“试探-退让-再试探”的规则。我刚开始调流媒体的时候,总觉得这玩意儿是网络层的事,跟应用层关系不大。后来被线上卡顿狠狠教育过一次,才明白——不懂拥塞控制,你连码率为什么跳都解释不清。

这一章我们拆开来看四个核心阶段。它们不是孤立的,而是一个完整的状态机。你想想看,TCP 发送方就像个瞎子,只能靠 ACK 和丢包来猜网络状况。那它怎么猜?靠的就是这几套算法。

3.1 慢启动:从 1 开始,指数增长

连接刚建立的时候,发送方不知道网络能扛多少。所以它很怂,从 cwnd = 1 个 MSS 开始发。每收到一个 ACK,cwnd 就加 1。这看起来是线性增长,但别忘了——每个 RTT 内 ACK 的数量也在翻倍。

举个例子:

  • 初始 cwnd = 1,发 1 个包,等 ACK
  • 收到 1 个 ACK → cwnd = 2,发 2 个包
  • 收到 2 个 ACK → cwnd = 4,发 4 个包
  • 收到 4 个 ACK → cwnd = 8,发 8 个包

每个 RTT 翻一倍。这就是指数增长。为什么叫“慢启动”?其实它一点也不慢,只是起点低。我个人习惯把这段时间叫做“网络摸底期”——摸清楚了再加速。

关键参数:ssthresh(慢启动阈值)

当 cwnd 达到 ssthresh 时,慢启动结束,进入拥塞避免。ssthresh 的初始值通常很高(比如 65535 字节),但每次丢包后会被重置为当前 cwnd 的一半。

我踩过的坑: 曾经在弱网环境下,慢启动还没跑完就超时了。后来发现是初始 RTO 设置太保守。建议在流媒体场景下,把初始 cwnd 适当调大(比如 10 个 MSS),减少首屏等待时间。

3.2 拥塞避免:线性增长,小心试探

过了 ssthresh,TCP 就换了个策略。它不再指数增长,而是每个 RTT 只增加 1 个 MSS。具体实现是:每收到一个 ACK,cwnd 增加 MSS / cwnd。这样算下来,一个 RTT 内刚好加 1。

为什么这么保守?因为网络已经接近瓶颈了。你再猛冲,路由器队列就要爆。拥塞避免阶段,说白了就是“贴着天花板走”。

我记得有一次调一个直播推流,码率一直在 2Mbps 和 500Kbps 之间剧烈抖动。抓包一看,cwnd 在拥塞避免阶段反复触顶丢包。后来把发送缓冲区调大了一倍,抖动就平滑了。原因很简单——缓冲区给了拥塞控制更多的反应时间。

阶段 增长方式 触发条件 典型场景
慢启动 指数增长(每 RTT 翻倍) 连接建立 / 超时重传后 连接初始、空闲恢复
拥塞避免 线性增长(每 RTT +1 MSS) cwnd ≥ ssthresh 稳定传输、接近瓶颈

3.3 快速重传:不等超时,提前行动

传统 TCP 靠超时重传(RTO)来恢复丢包。但 RTO 通常几百毫秒,对于实时流媒体来说太慢了。快速重传的思路是:收到 3 个重复的 ACK,就认为包丢了,不等超时直接重传。

为什么会收到重复 ACK?因为接收方收到乱序包时,会回复它期望的那个序号。比如期望 seq=100,结果来了 seq=101,那它还是回 ACK=100。连续来 3 个 seq=101、102、103,就会触发 3 个重复的 ACK=100。

注意: 快速重传只适用于单个丢包。如果连续丢多个包,或者发生超时,还是要走 RTO。我曾经在弱网环境下遇到过“伪重传”——因为网络延迟抖动导致重复 ACK 误判。后来加了延迟 ACK 阈值才解决。

3.4 快速恢复:丢包后不归零

快速重传之后,TCP 不会回到慢启动,而是进入快速恢复。这是 TCP Reno 的核心改进。它做了三件事:

  1. 把 ssthresh 设为当前 cwnd 的一半
  2. 把 cwnd 设为 ssthresh + 3(因为已经收到的 3 个重复 ACK 说明有 3 个包已经离开网络)
  3. 每收到一个重复 ACK,cwnd 临时加 1(相当于“偷”一点带宽)

等收到新的 ACK(非重复的),说明丢的包已经补上了,这时候把 cwnd 设为 ssthresh,进入拥塞避免。

你想想看,这个设计很聪明——丢包不一定是网络满了,可能只是短暂的抖动。快速恢复让 TCP 不用从头开始,保留了大部分吞吐量。

快速恢复 vs 慢启动:

  • 慢启动:丢包后 cwnd 归 1,从头开始
  • 快速恢复:丢包后 cwnd 减半,保留一半吞吐量

对于流媒体来说,快速恢复能减少码率骤降的幅度。我建议在 ABR 算法中,把快速恢复的 cwnd 变化映射到码率选择上,避免码率跳变太剧烈。

3.5 状态机总览

这四个阶段不是顺序执行的,而是根据事件切换。我画了一张状态机图,帮你理清关系:

TCP 拥塞控制状态机 TCP 拥塞控制状态机 慢启动 cwnd 指数增长 拥塞避免 cwnd 线性增长 快速恢复 cwnd 减半 + 3 超时重传 cwnd = 1 cwnd ≥ ssthresh 3 重复 ACK 新 ACK 到达 超时 重新开始 说明: • 实线箭头:正常状态切换(3 重复 ACK 触发快速重传+快速恢复) • 虚线箭头:超时触发,回到慢启动(cwnd 归 1,ssthresh 减半) • 快速恢复结束后,自动进入拥塞避免,不会回到慢启动

3.6 实际调优建议

光知道理论不够,得能落地。我总结了几条实战经验:

  • 初始 cwnd 调大: 默认 10 个 MSS 对于流媒体来说偏小。我建议根据 RTT 动态调整,RTT 小于 50ms 时可以设到 20-30 个 MSS。
  • ssthresh 不要设太高: 如果链路带宽有限,ssthresh 设太高会导致慢启动阶段频繁丢包。建议根据历史吞吐量动态计算。
  • 快速重传阈值可调: 默认 3 个重复 ACK。对于高丢包率的无线网络,可以降到 2 个,减少等待时间。
  • 注意 RTO 最小值: Linux 默认 RTOmin=200ms。对于低延迟场景(比如 WebRTC),建议降到 100ms 甚至更低。

我曾经踩过的坑: 有一次在 4G 网络下做直播,发现快速恢复阶段 cwnd 恢复太慢,导致码率一直上不去。后来排查发现是接收窗口(rwnd)限制了 cwnd 增长。记住——cwnd 不能超过 rwnd,两个窗口取最小值才是实际发送窗口。