TCP 的拥塞控制是怎么工作的?
简化版
拥塞控制解决「发太快压垮整个网络」的问题(流量控制是防压垮接收方,两者不同)。它靠一个拥塞窗口 cwnd动态调节发送速率,四个经典算法配合:慢启动(cwnd 指数增长探测带宽)→ 拥塞避免(到阈值后线性增长)→ 丢包时 快重传 + 快恢复(把窗口减半而不是归零)。核心思想:慢慢加速探测网络能力,一旦拥塞就退让。
详细版
发送方维护一个拥塞窗口 cwnd,实际发送窗口是 min(rwnd, cwnd)。cwnd 随网络状况动态变化,四个算法:
- 慢启动(Slow Start):连接初期 cwnd 从 1 个 MSS 开始,每收到一个 ACK 就 +1 个 MSS,于是每经过一个 RTT 翻倍(指数增长),快速探测网络能承受多少。增长到慢启动阈值 ssthresh 就转入拥塞避免。
- 拥塞避免(Congestion Avoidance):cwnd 达到 ssthresh 后,改成每个 RTT 只 +1(线性增长),谨慎地继续试探,避免增长太猛引发拥塞。
- 快重传(Fast Retransmit):收到 3 个重复 ACK,判断某个包丢了,立即重传,不等超时。
- 快恢复(Fast Recovery):配合快重传——把 ssthresh 设为当前 cwnd 的一半,cwnd 也设为 ssthresh(减半而非归 1),然后进入拥塞避免。因为能收到重复 ACK 说明网络还能传数据,没必要像超时那样彻底重来。
超时的处理更狠:如果是超时(比 3 个重复 ACK 严重),认为网络严重拥塞,ssthresh 减半、cwnd 直接回到 1,重新慢启动。
完整版教学
一、为什么需要拥塞控制
流量控制只考虑了接收方,但数据从发送方到接收方要经过一堆中间路由器和链路。如果所有发送方都全速发,中间链路会拥塞——路由器缓冲区排队、溢出、大量丢包,触发大量重传,进一步加剧拥塞,最终网络崩溃(拥塞崩溃)。
拥塞控制就是让发送方根据网络的拥塞程度自我约束:网络通畅就多发点,出现拥塞迹象(丢包)就主动退让。它是一种「为了整个网络好」的自律,和流量控制(为了接收方)互补。
二、慢启动为什么「慢」,其实增长很快
「慢启动」这名字有点误导——它增长其实是指数级的(每 RTT 翻倍),一点都不慢。它「慢」是指起点低:从 1 个 MSS 开始,而不是一上来就猛发。
为什么要从小开始?因为发送方不知道网络能承受多少。与其一上来就大发特发导致拥塞,不如从小规模试探、快速翻倍逼近网络容量。等到 cwnd 达到阈值 ssthresh(上次拥塞时的经验值),说明快到网络容量了,就切换成谨慎的线性增长(拥塞避免),别再翻倍冒进。
记忆点:慢启动 = 起点低、指数涨;拥塞避免 = 线性涨、谨慎试探。分界线是 ssthresh。
三、加性增、乘性减(AIMD)
拥塞控制的核心哲学是 AIMD(Additive Increase, Multiplicative Decrease,加性增乘性减):
- 加性增:没拥塞时,cwnd 每个 RTT 缓慢地 +1(线性),小心翼翼地占用更多带宽;
- 乘性减:一遇到拥塞(丢包),cwnd 大幅减半(乘以 1/2)。
「慢慢加、猛猛减」的不对称设计,是为了快速对拥塞让路、缓慢地争取带宽,让网络能稳定收敛、多个连接公平共享带宽。这是 TCP 拥塞控制稳定的数学基础。
四、快恢复:区分「轻微丢包」和「严重拥塞」
早期 TCP(Tahoe)不管什么丢包,一律 cwnd 归 1、重新慢启动,太保守——一个包丢了就把速度打回原点。
后来(Reno)引入快恢复,做了个重要区分:
- 收到 3 个重复 ACK(快重传触发):说明后续的包还在正常到达(否则不会有重复 ACK),网络只是轻微丢包,没那么糟。所以 cwnd 减半、进入拥塞避免,而不是归 1。
- 超时:连 ACK 都收不到了,说明网络严重拥塞。这才 cwnd 归 1、重新慢启动。
用「丢包的严重程度」来决定退让的力度,避免了对轻微丢包的过度反应,吞吐更平稳。
五、现代拥塞控制算法
经典的 Reno 基于「丢包」判断拥塞,但在高带宽、有随机丢包的现代网络里不够好。所以有了改进:
- CUBIC(Linux 默认):用三次函数调整 cwnd,在高带宽长距离网络下更高效;
- BBR(Google):不靠丢包,而是主动测量带宽和 RTT来建模网络,避免把缓冲区填满(bufferbloat),在有随机丢包的网络下表现更好。
面试能提一句「现代用 CUBIC/BBR,不再单纯靠丢包判断」是加分项。
六、常见误区
- ❌ 把拥塞控制和流量控制混为一谈——一个防压垮网络,一个防压垮接收方。
- ❌ 以为慢启动增长很慢——它是指数增长,只是起点低。
- ❌ 以为任何丢包都让 cwnd 归 1——只有超时才归 1,3 个重复 ACK 只减半(快恢复)。
- ❌ 以为拥塞窗口由接收方通告——cwnd 是发送方根据网络状况自己估算的,rwnd 才是接收方通告的。
六、常见误区与追问
| 考点 | 正确口径 |
|---|---|
| 慢启动 | cwnd 指数增长,快速探测可用带宽 |
| 拥塞避免 | 超过 ssthresh 后线性增长 |
| 丢包处理 | 超时更重,三次重复 ACK 可快重传快恢复 |
cwnd starts small
if cwnd < ssthresh: cwnd doubles per RTT
else: cwnd grows linearly
loss -> reduce cwnd
拥塞控制保护的是网络,不是接收端;它调的是拥塞窗口 cwnd。
- 误区:慢启动真的增长很慢。 慢启动初始窗口小,但每个 RTT 近似翻倍,增长速度其实很快。
- 误区:拥塞控制和流量控制是一回事。 拥塞控制看网络承载能力,流量控制看接收端缓冲能力。
- 误区:一丢包就说明接收端处理不过来。 丢包更常被 TCP 当作网络拥塞信号,触发窗口收缩。
- 追问:cwnd 和 rwnd 谁决定发送量? 发送端实际可发窗口通常取
min(cwnd, rwnd)。 - 追问:快重传为什么比超时轻? 收到重复 ACK 说明网络仍在传包,可能只是局部丢失,不必像超时那样重置到很小。
- 追问:现代算法如 BBR 有什么不同? BBR 更关注带宽和 RTT 的估计,不完全依赖丢包作为拥塞信号。
七、加强记忆
拥塞控制防止压垮网络,靠拥塞窗口 cwnd 调速:慢启动(从 1 起指数增长探测带宽)→ 到 ssthresh 转拥塞避免(线性增长)→ 丢包时快重传 + 快恢复(3 个重复 ACK 就把 cwnd 减半,而超时才归 1 重新慢启动)。核心哲学是 AIMD「加性增、乘性减」,现代算法有 CUBIC、BBR。它和流量控制的区别是防网络 vs 防接收方。