Skip to content

TCP 拥塞控制:慢启动、拥塞避免、快重传、快恢复

你可能遇到过这种情况:下班高峰期,明明道路很宽(带宽很大),但就是堵得一动不动。

网络也是如此——带宽再大,如果所有车都挤在一起,也会堵死。

TCP 的拥塞控制,就是解决网络拥塞问题的机制。

为什么需要拥塞控制?

拥塞(Network Congestion)是指网络中的数据包太多,超过了路由器的处理能力,导致:

  • 丢包
  • 延迟增加
  • 整个网络性能下降
无拥塞:
发送方 ──────────────────────────────> 接收方
路由器:轻松处理,延迟低

拥塞:
发送方 ───> 路由器 ───> 路由器 ───> 接收方

         队列爆满,丢包

拥塞控制的目标是:在避免网络拥塞的前提下,尽可能利用带宽

拥塞控制的四个算法

┌─────────────────────────────────────────────────────────────┐
│                   TCP 拥塞控制四部曲                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 慢启动(Slow Start)                                    │
│     窗口从小到大,指数增长                                    │
│                                                             │
│  2. 拥塞避免(Congestion Avoidance)                         │
│     接近瓶颈,线性增长                                       │
│                                                             │
│  3. 快重传(Fast Retransmit)                               │
│     丢包时快速重传,不等超时                                  │
│                                                             │
│  4. 快恢复(Fast Recovery)                                  │
│     丢包后快速恢复,而不是从头开始                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

慢启动(Slow Start)

核心思想

连接建立初期,TCP 不知道网络能承受多少数据。

慢启动让 cwnd(拥塞窗口)从一个很小的值开始,每收到一个 ACK,cwnd 就增加一点。

增长规则

初始:cwnd = 1 MSS(最大报文段大小)

每收到一个 ACK:cwnd += 1 MSS

等价于:每 RTT,cwnd 加倍
时间轴:
t=0:   cwnd = 1 MSS
t=1RTT: cwnd = 2 MSS   (+1)
t=2RTT: cwnd = 4 MSS   (+2)
t=3RTT: cwnd = 8 MSS   (+4)
t=4RTT: cwnd = 16 MSS  (+8)
...

什么时候停止慢启动?

停止条件:
1. cwnd >= ssthresh(慢启动阈值)
2. 发生丢包

进入拥塞避免阶段
ssthresh 的初始值:
- 通常是接收方通告窗口的大小
- 或者一个较大的值(如 65535 bytes)

图示

cwnd

16│                         ● 拥塞避免(线性增长)
  │                       ╱
 8│                     ●
  │                   ╱
 4│                 ●    ← ssthresh = 4
  │               ╱
 2│             ●      ← 慢启动结束
  │           ╱
 1│         ●          ← 慢启动开始
  └──────────────────────────────► 时间(RTT)
   1  2  4  8  ...

拥塞避免(Congestion Avoidance)

核心思想

当 cwnd 接近网络容量时,不能继续指数增长了,否则会加剧拥塞。

改为每 RTT 增加 1 MSS,线性增长。

增长规则

每收到一个 ACK:cwnd += MSS * MSS / cwnd

等价于:每 RTT,cwnd += 1 MSS

为什么是这样?

每个 ACK 确认了 cwnd / MSS 个字节
每个字节增加 1 MSS / cwnd

所以总增量 = (cwnd / MSS) * (MSS / cwnd) * MSS = MSS

拥塞避免过程

ssthresh = 8 MSS

cwnd = 8 MSS(进入拥塞避免)
t=1RTT: cwnd = 9 MSS   (+1)
t=2RTT: cwnd = 10 MSS  (+1)
t=3RTT: cwnd = 11 MSS  (+1)
t=4RTT: cwnd = 12 MSS  (+1)
...

拥塞时会发生什么?

丢包的两种表现

1. 超时丢包
   - 最严重的拥塞信号
   - 触发:慢启动从头开始
   
2. 快速重传(3个重复ACK)
   - 较轻的拥塞
   - 触发:快恢复

超时丢包处理

检测到超时:
1. ssthresh = cwnd / 2
2. cwnd = 1 MSS
3. 回到慢启动阶段

这是最保守的处理,因为超时意味着网络可能已经严重拥塞。

快速重传处理

收到 3 个重复 ACK:
1. ssthresh = cwnd / 2
2. cwnd = ssthresh + 3 * MSS
3. 进入快恢复

和超时不同,快速重传说明至少有些数据到了接收方,网络情况没那么糟。

快重传(Fast Retransmit)

核心思想

不要等超时(太慢),用重复 ACK 来快速检测丢包。

丢包场景:

发送方 ──── 1 ────> 收到
发送方 ──── 2 ────> 收到
发送方 ──── 3 ────> 丢失!
发送方 ──── 4 ────> 收到

接收方 ──── ACK(2) ────> 重复 ACK
接收方 ──── ACK(2) ────> 重复 ACK(3个)
接收方 ──── ACK(2) ────> 重复 ACK

收到 3 个重复 ACK,立即重传 seq=3

为什么是 3 个?

1 个重复 ACK 可能是乱序 2 个重复 ACK 不够确定 3 个重复 ACK 基本可以确定是丢包

快恢复(Fast Recovery)

核心思想

丢包后,不要完全从头开始,而是快速恢复。

Reno 算法的快恢复

收到 3 个重复 ACK:
1. ssthresh = cwnd / 2
2. cwnd = ssthresh + 3 * MSS
3. 重传丢失的段
4. 如果还有重复 ACK,cwnd += MSS
5. 收到新数据的 ACK,退出快恢复,进入拥塞避免
图示:
                    快恢复
cwnd ────●──── ssthresh + 3 ────────── cwnd = ssthresh
         ↑          ↓                    ↓
       丢包       cwnd -= 2              线性增长

Tahoe 算法的快恢复(旧版)

Tahoe 算法收到 3 个重复 ACK 后,直接回到慢启动:

Tahoe:
收到 3 个 dup ACK → ssthresh = cwnd/2 → cwnd = 1 MSS → 慢启动

Reno:
收到 3 个 dup ACK → ssthresh = cwnd/2 → cwnd = ssthresh + 3*MSS → 快恢复

Reno 比 Tahoe 更好,因为快恢复允许更激进地恢复。

综合流程图

                    cwnd

                      │        ┌─────────────────────┐
                      │        │    拥塞避免          │
                      │        │   线性增长            │
                      │        │   (cwnd += 1/RTT)    │
                      │        └─────────────────────┘
                      │               ▲
                      │               │ cwnd >= ssthresh
                      │               │
                      │        ┌──────┴──────┐
                      │        │             │
                      │        │ 慢启动      │  收到 3 dup ACK
                      │        │ 指数增长    │─────────────┐
                      │        │ cwnd *= 2   │             │
                      │        │             │             ▼
                      │        └─────────────┘    ┌─────────────┐
                      │                          │   快恢复    │
                      │                          │ (cwnd/2+N) │
                      │                          └─────────────┘
                      │                                   ▲
                      │                                   │ 收到 ACK
                      │                                   │
                      └───────────────────────────────────┘
                              超时 → cwnd = 1, ssthresh = cwnd/2

实际案例:拥塞窗口观察

bash
# Linux 查看 TCP 拥塞控制算法
cat /proc/sys/net/ipv4/tcp_congestion_control
# 输出:cubic(CentOS/RHEL 默认)

# 查看可用的拥塞控制算法
cat /proc/sys/net/ipv4/tcp_available_congestion_control
# 输出:cubic reno

# 切换算法(需要权限)
echo reno > /proc/sys/net/ipv4/tcp_congestion_control
java
// Java 设置 socket 选项(部分支持)
Socket socket = new Socket();
socket.setPerformancePreferences(0, 2, 1);
// 参数含义:短连接延迟、带宽、连接时间

现代拥塞控制算法

CUBIC(Linux 默认)

CUBIC 的窗口增长函数:

W(t) = C * (t - K)^3 + W_max

C: 常数
t: 从丢包后经过的时间
K: 窗口从 W_max 降到 1 的时间
W_max: 丢包前的窗口大小

CUBIC 比 Reno 更保守,在高带宽网络中表现更好。

BBR(Bottleneck Bandwidth and RTT)

BBR 是 Google 2016 年提出的新算法,不依赖丢包来检测拥塞。

BBR 的核心思想:
1. 持续测量带宽(最大带宽)
2. 持续测量 RTT(最小延迟)
3. 窗口 = 带宽 * RTT(BDP,带宽延迟积)
与 CUBIC 的区别:

CUBIC:丢包 → 减少窗口 → 慢慢恢复
BBR:测量带宽和延迟 → 主动控制窗口
BBR 优势:
- 在高延迟/丢包网络中表现更好
- 不依赖丢包来控制拥塞
- 对无线网络更友好

BBR 劣势:
- 需要更多内存跟踪状态
- 在某些场景下可能占用太多缓冲区

面试追问方向

  • 什么是拥塞控制?为什么需要拥塞控制?
  • 拥塞控制和流量控制的区别是什么?
  • 慢启动阶段 cwnd 是如何增长的?
  • 什么情况下会退出慢启动进入拥塞避免?
  • 什么是快重传?为什么要等 3 个重复 ACK?
  • 快恢复和慢启动的区别是什么?
  • 什么是 BDP(带宽延迟积)?
  • CUBIC 和 BBR 的区别是什么?
  • TCP 连接建立初期,为什么要用慢启动而不是直接用大窗口?
  • 如何查看和修改系统的拥塞控制算法?

基于 VitePress 构建