土法炼钢兴趣小组的算法知识备份

【网络工程】TCP 拥塞控制经典算法:从 Reno 到 CUBIC

文章导航

分类入口
network
标签入口
#tcp#congestion-control#cubic#reno#cwnd

目录

你把服务从单机房迁移到了跨洲际部署。RTT 从 1ms 变成 200ms。带宽明明是 1Gbps,但 iperf3 测下来只有几十 Mbps。你检查了流量控制——接收窗口 16MB,远超 BDP——问题不在那里。

问题在拥塞控制。

上一篇我们讲了流量控制——接收方控制发送方的速率。拥塞控制解决的是另一个问题:网络本身能承受多少流量? 发送方并不知道路径上有多少路由器、多少队列、多少竞争流量。它只能通过观察丢包和延迟来”猜测”网络容量,然后调整自己的发送速率。

这个猜测过程就是拥塞控制算法。从 1988 年 Jacobson 提出 TCP Tahoe 开始,到今天 Linux 默认的 CUBIC,经历了 30 多年的演进。每一代算法都在试图更好地回答同一个问题:在不压垮网络的前提下,怎么尽可能快地发送数据?

一、拥塞控制的基本框架

为什么需要拥塞控制

没有拥塞控制的网络会发生什么?1986 年 10 月,互联网经历了第一次”拥塞崩溃”(Congestion Collapse):

# 1986 年 LBL → UC Berkeley 的链路吞吐量变化
# 物理带宽:32Kbps
# 正常吞吐:32Kbps
# 拥塞崩溃后吞吐:40bps  ← 下降了近 1000 倍!

# 原因链:
# 1. 多个 TCP 连接同时全速发送
# 2. 路由器队列溢出 → 大量丢包
# 3. TCP 重传 → 更多流量 → 更多丢包
# 4. 恶性循环:重传流量占据了所有带宽,有效吞吐趋近于零

# 这就是拥塞崩溃:
# 物理链路满负载,但有效传输几乎为零
# 所有带宽都被"无用"的重传包占满

Van Jacobson 在 1988 年的论文 “Congestion Avoidance and Control” 中提出了解决方案——这奠定了所有现代拥塞控制算法的基础。

拥塞窗口(cwnd)

拥塞控制的核心变量是拥塞窗口(Congestion Window, cwnd)——发送方自己维护的一个值,表示”我认为现在可以同时在网络中飞行的数据量”:

# 有效发送窗口 = min(rwnd, cwnd)
#
# rwnd:接收方告诉你的(流量控制)
# cwnd:自己估算的(拥塞控制)
# 最终取小值:不能超过接收方的能力,也不能超过网络的能力

# 用 ss 观察 cwnd
ss -ti dst 10.0.0.2
# ESTAB  0  0  10.0.0.1:443  10.0.0.2:54321
#   cubic wscale:7,7 rto:204 rtt:0.5/0.02 ato:40 mss:1448
#   cwnd:10 ssthresh:65535 bytes_sent:156844 bytes_acked:156844
#   send 231.7Mbps lastrcv:24 lastack:24 pacing_rate 463.2Mbps

# cwnd:10 — 当前拥塞窗口为 10 个 MSS(10 × 1448 = 14480 字节)
# ssthresh:65535 — 慢启动阈值(下面详解)
# send 231.7Mbps — 当前估算的发送速率

# cwnd 的单位是 MSS(最大段长度),不是字节
# 在上面的例子中:
# 最大飞行数据 = cwnd × MSS = 10 × 1448 = 14,480 字节

AIMD:拥塞控制的核心策略

几乎所有经典拥塞控制算法都基于 AIMD(Additive Increase / Multiplicative Decrease)策略:

# AIMD 的数学直觉:
#
# 加性增长(Additive Increase):
#   每个 RTT,cwnd += 1 MSS
#   → 线性增长,温和地探测网络容量
#
# 乘性减少(Multiplicative Decrease):
#   检测到丢包时,cwnd = cwnd × β(β 通常为 0.5)
#   → 快速让出带宽,避免加剧拥塞
#
# 为什么不是 MIMD 或 AIAD?
# MIMD(乘增乘减):增长太激进,容易引发拥塞
# AIAD(加增加减):收敛太慢,多条流无法达到公平性
# AIMD 是唯一能保证"效率"和"公平性"同时收敛的策略
# (Chiu & Jain, 1989 的证明)

# AIMD 的公平性收敛过程(两条竞争流):
# 假设两条流 A 和 B 共享一条带宽为 C 的链路
#
#     带宽 B ↑     公平线 (A = B)
#            |    /
#            |   / ← AIMD 轨迹沿效率线锯齿
#            |  /     趋向公平线收敛
#            | /
#    --------/----------→ 带宽 A
#            效率线 (A + B = C)
#
# 两条流的 AIMD 锯齿轨迹会自然收敛到公平点

拥塞控制的四个阶段

经典 TCP 拥塞控制包含四个核心机制:

# 1. 慢启动(Slow Start)
#    - 初始 cwnd = initcwnd(Linux 默认 10 MSS, RFC 6928)
#    - 每收到一个 ACK, cwnd += 1 MSS
#    - 效果:每个 RTT, cwnd 翻倍(指数增长)
#    - 直到 cwnd >= ssthresh 或检测到丢包

# 2. 拥塞避免(Congestion Avoidance)
#    - 当 cwnd >= ssthresh 时进入
#    - 每个 RTT, cwnd += 1 MSS(线性增长)
#    - 温和地探测剩余容量

# 3. 快速重传(Fast Retransmit)
#    - 收到 3 个重复 ACK → 判断为丢包
#    - 立即重传丢失的段(不等 RTO 超时)

# 4. 快速恢复(Fast Recovery)
#    - 丢包后 cwnd 减半(而不是回到 1)
#    - 避免重新慢启动的性能损失
stateDiagram-v2
    [*] --> SlowStart: 连接建立<br>cwnd = initcwnd
    SlowStart --> CongestionAvoidance: cwnd ≥ ssthresh
    SlowStart --> FastRecovery: 3 dup ACKs<br>ssthresh = cwnd/2
    SlowStart --> SlowStart2: RTO 超时<br>ssthresh = cwnd/2<br>cwnd = 1
    
    CongestionAvoidance --> FastRecovery: 3 dup ACKs<br>ssthresh = cwnd/2
    CongestionAvoidance --> SlowStart2: RTO 超时<br>ssthresh = cwnd/2<br>cwnd = 1
    
    FastRecovery --> CongestionAvoidance: 新 ACK<br>cwnd = ssthresh
    FastRecovery --> SlowStart2: RTO 超时<br>ssthresh = cwnd/2<br>cwnd = 1
    
    SlowStart2 --> CongestionAvoidance: cwnd ≥ ssthresh
    SlowStart2 --> FastRecovery: 3 dup ACKs

    state SlowStart2 <<choice>>
    
    note right of SlowStart: 指数增长:每 RTT cwnd 翻倍
    note right of CongestionAvoidance: 线性增长:每 RTT cwnd += 1
    note left of FastRecovery: cwnd = ssthresh + 3\n收到 dup ACK 时 cwnd++

二、慢启动:名字骗人的指数增长

慢启动并不慢

“慢启动”这个名字极具误导性——它实际上是指数增长

# 慢启动过程(initcwnd = 10, MSS = 1460 bytes)

# RTT 0: cwnd = 10, 发送 10 个段(14,600 字节)
#         收到 10 个 ACK → cwnd 变为 20

# RTT 1: cwnd = 20, 发送 20 个段(29,200 字节)
#         收到 20 个 ACK → cwnd 变为 40

# RTT 2: cwnd = 40, 发送 40 个段(58,400 字节)
#         收到 40 个 ACK → cwnd 变为 80

# RTT 3: cwnd = 80, 发送 80 个段(116,800 字节)

# 增长速度:10 → 20 → 40 → 80 → 160 → ...
# 每个 RTT 翻倍,3 个 RTT 后发送速率就超过了 100KB
# "慢"只是相对于"一上来就全速发"

# 它叫"慢启动"是因为 Jacobson 论文的上下文:
# 之前的 TCP 一建立连接就按 rwnd 全速发("fast start")
# 从 1 开始指数增长,相对于全速,确实"慢"了

initcwnd 的工程影响

初始拥塞窗口(Initial Congestion Window, initcwnd)决定了 TCP 连接在第一个 RTT 能发送多少数据:

# Linux 的 initcwnd 默认值是 10(RFC 6928, 2013 年标准化)
# 历史演进:
# RFC 2581 (1999): initcwnd = 1
# RFC 3390 (2002): initcwnd = min(4, max(2, 4380/MSS))
# RFC 6928 (2013): initcwnd = 10

# 查看当前 initcwnd
ip route show default
# default via 10.0.0.1 dev eth0 proto dhcp src 10.0.0.100 metric 100
# 没有显式 initcwnd → 使用内核默认值 10

# 修改 initcwnd(需要 root)
ip route change default via 10.0.0.1 dev eth0 initcwnd 20

# 验证
ip route show default
# default via 10.0.0.1 dev eth0 initcwnd 20

# initcwnd = 10 时,第一个 RTT 能发送的数据:
# 10 × 1460 bytes = 14,600 bytes ≈ 14.3 KB
# 对于小型 API 响应(< 14KB),一个 RTT 就能发完
# 对于典型的网页首屏(~50-100KB),需要 3-4 个 RTT

# Google 的研究(2010)发现 initcwnd = 10 的收益:
# - 减少平均页面加载时间 ~10%
# - 对高 RTT 连接收益更大(减少慢启动的 RTT 轮次)
# - 增加丢包率 < 0.5%(可接受)

慢启动阈值(ssthresh)

ssthresh 是慢启动和拥塞避免的分界线——它记录了”上一次出问题时的窗口大小”:

# ssthresh 的语义:
# "我上次用这么大的窗口时出了问题(丢包),所以这次到这个值就减速"

# 初始值:通常是一个很大的值(65535 或 int_max)
# 更新时机:每次丢包时,ssthresh = cwnd / 2

# 观察 ssthresh
ss -ti | grep ssthresh
# cubic wscale:7,7 rto:204 rtt:0.5/0.02
#   cwnd:32 ssthresh:28 bytes_sent:1568440

# ssthresh:28 说明之前发生过丢包
# 丢包时 cwnd ≈ 56, ssthresh 设为 56/2 = 28
# 现在 cwnd:32 > ssthresh:28, 处于拥塞避免阶段(线性增长)

# 如果 ssthresh 显示很大的值(如 2147483647)
# 说明这条连接还没有经历过丢包

三、TCP Tahoe 与 Reno:第一代拥塞控制

TCP Tahoe(1988)

Tahoe 是 Jacobson 在 4.3BSD Tahoe 版本中实现的第一个拥塞控制算法:

# Tahoe 的行为:
#
# 慢启动 → 拥塞避免 → 检测到丢包 → cwnd = 1, 重新慢启动
#
# 丢包检测方式:
# 1. RTO 超时
# 2. 3 个重复 ACK(快速重传)
#
# 不管哪种丢包信号,Tahoe 都是:
# ssthresh = cwnd / 2
# cwnd = 1(回到初始值!)
# 重新从慢启动开始
#
# 问题:每次丢包都回到 1,性能恢复需要多个 RTT
# 在高带宽链路上,从 cwnd=1 重新爬升到合适的窗口需要很长时间

TCP Reno(1990)

Reno 的关键改进是区分了两种丢包信号:

# Reno 的核心改进:快速恢复(Fast Recovery)
#
# RTO 超时(严重拥塞):
#   → ssthresh = cwnd / 2
#   → cwnd = 1
#   → 重新慢启动(和 Tahoe 一样)
#
# 3 个重复 ACK(轻微拥塞):
#   → ssthresh = cwnd / 2
#   → cwnd = ssthresh + 3(不是回到 1!)
#   → 进入快速恢复
#   → 每收到一个重复 ACK, cwnd += 1
#   → 收到新 ACK 时, cwnd = ssthresh, 进入拥塞避免
#
# 为什么 + 3?
# 收到 3 个重复 ACK 说明有 3 个段已经到达接收方
# 它们已经"离开了网络",所以可以把它们从飞行中数据量中扣除

# Reno 的 cwnd 变化示意:
#
# cwnd
# 40 |          /\
# 35 |         /  \
# 30 |        /    \
# 25 |       /      \
# 20 |      /        \------------ ← 快速恢复后 cwnd = ssthresh
# 15 |     /          丢包 ↑
# 10 |    /        ssthresh = 20
#  5 |   /
#  1 |--/
#    +----------------------------→ 时间
#      慢启动   拥塞避免  快速恢复   拥塞避免

Reno 的局限

# Reno 对多个包丢失的处理有严重问题:

# 场景:一个窗口中丢了 2 个包(包 5 和包 10)

# Reno 的行为:
# 1. 收到 3 个关于包 5 的 dup ACK → 快速重传包 5
# 2. 进入快速恢复, cwnd 减半
# 3. 包 5 的重传被确认 → 退出快速恢复, cwnd = ssthresh
# 4. 现在发现包 10 也丢了 → 又收到 3 个 dup ACK
# 5. 再次进入快速恢复, cwnd 再减半!
# → 一个窗口丢 2 个包, cwnd 被减了两次
# → 如果丢了 N 个包, cwnd 被减 N 次 → 性能急剧下降

# 这就是 Reno 的"多包丢失"问题
# 在突发性丢包的网络中(如无线网络),性能表现很差

四、NewReno:修复多包丢失

NewReno 的改进(RFC 6582)

# NewReno 的核心改进:快速恢复期间不退出

# Reno: 收到新 ACK → 立即退出快速恢复
# NewReno: 收到的 ACK 只是部分确认(Partial ACK)→ 继续留在快速恢复

# Partial ACK:确认了一些但不是全部飞行中的数据

# NewReno 的行为(同样丢了包 5 和包 10):
# 1. 收到 3 个关于包 5 的 dup ACK → 快速重传包 5
# 2. 进入快速恢复
# 3. 包 5 被确认,但 ACK 只确认到包 9(Partial ACK)
#    → NewReno 认识到还有包丢了
#    → 立即重传包 10
#    → 继续留在快速恢复,不减 cwnd
# 4. 包 10 被确认,ACK 确认了所有飞行中数据
#    → 退出快速恢复
# → cwnd 只减了一次!性能好得多

# NewReno 相比 Reno 的改进:
# - 不需要 SACK 支持(所以在不支持 SACK 的环境中特别有价值)
# - 但每个 RTT 只能恢复一个丢包(因为不知道哪些包丢了)
# - 如果丢了 N 个包,需要 N 个 RTT 才能恢复
# - 有 SACK 的环境下, SACK + Reno 比纯 NewReno 更好

五、BIC 与 CUBIC:高速网络的拥塞控制

为什么需要新算法

Reno/NewReno 的 AIMD 在高 BDP(Bandwidth-Delay Product)网络中有致命问题:

# 问题:Reno 的线性增长在高 BDP 网络中太慢

# 场景:10Gbps 链路, RTT = 100ms
# BDP = 10Gbps × 100ms = 125MB
# MSS = 1460 bytes
# 需要 cwnd = 125MB / 1460 ≈ 89,726 个 MSS

# Reno 拥塞避免的增长速度:每 RTT cwnd += 1
# 从 cwnd = 44,863 (丢包后减半) 恢复到 89,726 需要:
# 89,726 - 44,863 = 44,863 个 RTT
# 44,863 × 100ms = 4,486 秒 ≈ 75 分钟!

# 一次丢包后需要 75 分钟才能恢复到满带宽利用率
# 这在工程上完全不可接受

# 各算法在 10Gbps / 100ms 链路上的恢复时间:
# Reno:  ~75 分钟
# BIC:   ~4 分钟(二分搜索)
# CUBIC: ~3 分钟(三次函数增长)

BIC(Binary Increase Congestion Control)

BIC 使用二分搜索(Binary Search)来更快地找到最优窗口:

# BIC 的核心思想:
# 丢包时的 cwnd = Wmax(上次能工作的最大窗口)
# 丢包后 cwnd 减半 = Wmin(安全窗口)
# 用二分搜索在 Wmin 和 Wmax 之间找最优点

# BIC 的增长过程:
# 1. cwnd 从 Wmin 开始
# 2. 目标:(Wmin + Wmax) / 2 = 中点
# 3. 增加到中点后,如果没有丢包:
#    → Wmin = 中点, 继续向 Wmax 二分
# 4. 如果丢包:
#    → Wmax = 当前 cwnd, 重新二分
#
# 每次二分跳跃的步长是固定的 max increment
# 当离 Wmax 很远时:大步快跑(additive increase,步长为 Smax)
# 当离 Wmax 很近时:小步探测(二分搜索, 步长递减)

# BIC cwnd 增长曲线:
#
# cwnd
# Wmax|....................*
#     |                 *
#     |              *     ← 接近 Wmax 时减速(二分搜索)
#     |           *
#     |        *           ← 中间阶段
#     |     *
#     |  *                 ← 远离时加速(additive increase)
# Wmin|*
#     +----------------------------→ 时间

# BIC 的问题:
# 1. 在低速链路上太激进(RTT 不公平)
# 2. 增长函数不连续(在 additive 和 binary 之间切换)

CUBIC:Linux 默认的拥塞控制算法

CUBIC 是 BIC 的改进版,也是 Linux 自 2.6.19 (2006) 以来的默认拥塞控制算法:

# CUBIC 的核心公式:
# W(t) = C × (t - K)³ + Wmax
#
# 其中:
# W(t) = 时间 t 处的 cwnd
# C = 缩放常数(Linux 中 C = 0.4)
# K = 从 Wmin 增长到 Wmax 需要的时间
#   K = ∛(Wmax × β / C)   (β = 0.3 for CUBIC, β = 0.5 for Reno)
# Wmax = 上次丢包时的 cwnd
# t = 从上次丢包到现在的时间

# CUBIC 的名字来自这个三次函数(Cubic Function)

# β = 0.7(CUBIC 的乘性减少因子)
# 丢包时 cwnd = Wmax × 0.7(只减 30%,不像 Reno 减 50%)
# 这意味着 CUBIC 在丢包后恢复更快

# CUBIC 的增长曲线特征:
#
# cwnd
# Wmax ├──────────────────────────*  ← 超过 Wmax 后继续凸增长
#      │                    * .
#      │                 *  .     ← 接近 Wmax 时平坦(保守探测)
#      │              *    .
#      │           *      .       ← 凹增长(快速恢复)
#      │        *        .
#      │     *          .
#      │  *            .
# Wmin ├*             .
#      ├──────────────────────────→ 时间
#         凹(concave)  平  凸(convex)
#
# 关键特征:
# - 远离 Wmax 时快速增长(凹,concave)
# - 接近 Wmax 时减速(平坦段,稳定探测)
# - 超过 Wmax 后缓慢探测新容量(凸,convex)

CUBIC vs Reno 的增长对比

# 在 ss 中观察拥塞控制算法和 cwnd 变化

# 实时监控 cwnd 变化
watch -n 0.1 'ss -ti dst 10.0.0.2 | grep -E "cwnd|cubic|reno"'

# 更精细的 cwnd 时间序列
while true; do
    echo "$(date +%s.%N) $(ss -ti dst 10.0.0.2 | grep -oP 'cwnd:\K[0-9]+')"
    sleep 0.01
done > /tmp/cwnd_trace.log

# 查看当前使用的拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# net.ipv4.tcp_congestion_control = cubic

# 查看可用的算法
sysctl net.ipv4.tcp_available_congestion_control
# net.ipv4.tcp_available_congestion_control = reno cubic

# 查看已注册(含模块)的算法
sysctl net.ipv4.tcp_allowed_congestion_control
# net.ipv4.tcp_allowed_congestion_control = reno cubic

CUBIC 的 RTT 公平性

CUBIC 相比 BIC 的一个关键改进是 RTT 公平性

# BIC 的 RTT 不公平问题:
# BIC 的窗口增长和 RTT 相关(每 RTT 增长一次)
# RTT 小的流每单位时间增长更多次 → 抢占更多带宽
# 这对高 RTT 的长距离流非常不利

# CUBIC 的解决方案:
# CUBIC 的窗口增长是关于"时间"的函数,不是关于"RTT"的函数
# W(t) = C × (t - K)³ + Wmax
# t 是真实时间,不是 RTT 的计数
# 所以不管 RTT 是 1ms 还是 100ms,窗口增长曲线一样
# → RTT 公平性好得多

# 但完全公平是不可能的:
# RTT 更小的流确认回来得更快 → 触发更频繁的窗口更新
# CUBIC 通过"以时间为基准"大大减轻了这个问题
# 但在极端 RTT 差异下(1ms vs 300ms),仍然不完全公平

# 实测验证 CUBIC 的 RTT 公平性
# 用 iperf3 启动两条流,RTT 分别为 10ms 和 100ms
# 观察带宽分配比例
# CUBIC: ~2:1(轻微不公平)
# Reno:  ~10:1(严重不公平)

CUBIC TCP 友好模式

CUBIC 还有一个”TCP 友好”模式,确保它在低 BDP 网络中不会比 Reno 更差:

# CUBIC 的 TCP 友好模式(tcp_friendliness)
#
# 计算 Reno 在相同条件下的 cwnd 增长:
# W_reno(t) = Wmax × (1 - β) + 3β/(2 - β) × (t / RTT)
#
# 如果 W_reno(t) > W_cubic(t):
#   → 使用 W_reno(t)(切换到标准 Reno 行为)
# 否则:
#   → 使用 W_cubic(t)(CUBIC 三次函数增长)
#
# 这保证了 CUBIC 在任何网络条件下至少和 Reno 一样好
# 在低 BDP 网络中,CUBIC 实质上退化为 Reno

# Linux 内核中可以通过模块参数调整 CUBIC 行为
# 查看 CUBIC 模块参数
find /sys/module/tcp_cubic -type f 2>/dev/null
# /sys/module/tcp_cubic/parameters/beta
# /sys/module/tcp_cubic/parameters/hystart
# /sys/module/tcp_cubic/parameters/hystart_detect
# /sys/module/tcp_cubic/parameters/hystart_ack_delta_us
# /sys/module/tcp_cubic/parameters/fast_convergence
# /sys/module/tcp_cubic/parameters/initial_ssthresh
# /sys/module/tcp_cubic/parameters/tcp_friendliness

# 查看参数值
cat /sys/module/tcp_cubic/parameters/beta
# 717   ← 实际 β = 717/1024 ≈ 0.7

cat /sys/module/tcp_cubic/parameters/tcp_friendliness
# 1     ← TCP 友好模式开启

六、HyStart:优化慢启动退出

传统慢启动的问题

慢启动以指数增长——它的退出条件是”丢包”。但当 cwnd 翻倍式增长到超过 BDP 时,可能一下子涌入大量超额数据,导致路由器队列溢出和大量丢包:

# 问题场景(BDP = 100 个 MSS):
# RTT 4: cwnd = 64  → 正常
# RTT 5: cwnd = 128 → 超过 BDP! 路由器队列瞬间溢出
# → 大量丢包, ssthresh = 64, cwnd 大幅下降

# 超调量:128 - 100 = 28 个 MSS 超额(28% 的超调)
# 这些超额数据会被丢弃,造成不必要的重传和延迟

HyStart(Hybrid Slow Start)

HyStart 是 CUBIC 附带的慢启动优化,通过延迟增长信号来判断是否即将到达 BDP:

# HyStart 的核心思想:
# 不等到丢包才退出慢启动
# 通过观察 RTT 的增长趋势,提前检测到"队列开始积累"
# 在丢包发生之前就切换到拥塞避免

# HyStart 的两个检测条件:
#
# 1. ACK Train 检测(已从 Linux 5.7 移除)
#    监测一轮 ACK 中最后一个 ACK 和第一个 ACK 的间隔
#    如果间隔 > min_RTT / 8 → 可能在排队
#
# 2. 延迟增长检测(当前默认使用)
#    监测当前 RTT 和最小 RTT 的差值
#    如果 RTT_current - RTT_min > 一个阈值 → 队列在增长

# HyStart 的参数(Linux)
cat /sys/module/tcp_cubic/parameters/hystart
# 1     ← HyStart 开启

cat /sys/module/tcp_cubic/parameters/hystart_detect
# 3     ← 检测模式(bitmap: 1=ACK train, 2=delay, 3=both)

# HyStart++ (RFC 9406, Linux 5.7+)
# 改进:不直接进入拥塞避免,而是进入"Conservative Slow Start"
# CSS 阶段:每隔一个 RTT 才将 cwnd 翻倍(减缓增长到 1/2 速率)
# 如果 CSS 阶段连续几个 RTT 未检测到延迟增长 → 恢复正常慢启动
# 如果持续检测到延迟增长 → 进入拥塞避免

七、算法实测与观测

用 ss 实时观测拥塞控制状态

# ss -ti 提供的拥塞控制信息(全量字段解读)
ss -ti dst 10.0.0.2

# 典型输出:
# cubic wscale:7,7 rto:204 rtt:1.2/0.5 ato:40 mss:1448
#   cwnd:42 ssthresh:38 bytes_sent:4506880 bytes_retrans:14480
#   bytes_acked:4492400 bytes_received:156844
#   send 406.9Mbps lastrcv:24 lastack:24 pacing_rate 488.3Mbps
#   delivery_rate 380.2Mbps app_limited busy:2400ms
#   rcv_space:29200 rcv_ssthresh:64088 minrtt:0.8

# 关键字段解读:
# cubic          — 使用的拥塞控制算法
# cwnd:42        — 当前拥塞窗口(42 × MSS = 60,816 bytes)
# ssthresh:38    — 慢启动阈值(说明发生过丢包)
# rtt:1.2/0.5    — 平滑 RTT / RTT 方差(单位 ms)
# send 406.9Mbps — 估算发送速率 = cwnd × MSS / RTT
# pacing_rate    — 实际 pacing 速率(TCP pacing 用于均匀发送)
# delivery_rate  — 实际交付速率
# app_limited    — 应用没有足够数据发送(cwnd 没有被用满)
# minrtt:0.8     — 连接建立以来的最小 RTT
# busy:2400ms    — 连接有数据在传输的总时间

切换拥塞控制算法

# 系统级切换(影响所有新连接)
sysctl -w net.ipv4.tcp_congestion_control=cubic

# 查看支持的算法(含内核模块)
cat /proc/sys/net/ipv4/tcp_available_congestion_control
# reno cubic

# 加载其他算法模块
modprobe tcp_bbr
cat /proc/sys/net/ipv4/tcp_available_congestion_control
# reno cubic bbr

# 应用级切换(只影响特定 socket)
# C:
# int cc = TCP_CONGESTION;
# setsockopt(sockfd, IPPROTO_TCP, TCP_CONGESTION, "cubic", 5);

# Python:
# sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_CONGESTION, b'cubic')

# Go:
# syscall.SetsockoptString(fd, syscall.IPPROTO_TCP, 
#     syscall.TCP_CONGESTION, "cubic")

# 验证 socket 使用的算法
# ss -ti 输出的第一个字段就是算法名称

用 iperf3 实测拥塞控制差异

# === 测试环境 ===
# 服务端和客户端之间的链路:RTT 100ms, 带宽 100Mbps
# 使用 tc netem 模拟延迟(如果在同一网络中)

# 服务端添加 100ms 延迟
tc qdisc add dev eth0 root netem delay 50ms
# 双向各加 50ms,RTT ≈ 100ms

# === 测试 CUBIC ===
sysctl -w net.ipv4.tcp_congestion_control=cubic
iperf3 -c 10.0.0.2 -t 30 -i 1 --json > /tmp/cubic.json

# === 测试 Reno ===
sysctl -w net.ipv4.tcp_congestion_control=reno
iperf3 -c 10.0.0.2 -t 30 -i 1 --json > /tmp/reno.json

# === 对比结果 ===
# 从 JSON 中提取平均吞吐量
jq '.end.sum_sent.bits_per_second / 1e6' /tmp/cubic.json
# 94.2  ← CUBIC: 接近线速

jq '.end.sum_sent.bits_per_second / 1e6' /tmp/reno.json
# 87.3  ← Reno: 低 ~7%

# 在更高 BDP 的环境中差距更大:
# RTT 200ms, 带宽 1Gbps:
# CUBIC: ~920 Mbps
# Reno:  ~400 Mbps  ← Reno 的线性增长无法充分利用带宽

# 清理 netem
tc qdisc del dev eth0 root

八、经典算法对比

算法特性对比表

特性 Tahoe Reno NewReno CUBIC
年份 1988 1990 1999 2008
丢包响应(3 dup ACK) cwnd = 1 cwnd /= 2 cwnd /= 2 cwnd × 0.7
丢包响应(RTO) cwnd = 1 cwnd = 1 cwnd = 1 cwnd = 1
快速恢复 改进版
多包丢失处理
高 BDP 适应
RTT 公平性
增长函数 线性 线性 线性 三次函数
Linux 默认 ✓ (2.6.19+)

增长速度对比

# 丢包后从 cwnd/2 恢复到 cwnd 的 RTT 数(假设无再次丢包)
#
# Reno:    需要 cwnd/2 个 RTT(线性增长, 每 RTT +1)
# CUBIC:   需要 ~K RTT(三次函数, K = ∛(Wmax×β/C))
#
# 示例:Wmax = 1000 MSS
# Reno:  500 RTT
# CUBIC: ∛(1000 × 0.3 / 0.4) ≈ ∛750 ≈ 9 RTT
#        然后大约 3K ≈ 27 RTT 恢复到 Wmax 附近
#
# CUBIC 快了约 18 倍!

# 但 CUBIC 的增长不是始终快于 Reno:
# - 在 Wmax 附近,CUBIC 会放缓(三次函数的平坦段)
# - 在低 BDP 网络中,CUBIC 的 TCP 友好模式使其退化为 Reno
# - CUBIC 的优势主要体现在高 BDP 场景

九、拥塞控制的信号类型

基于丢包 vs 基于延迟

所有上面讨论的算法(Tahoe / Reno / NewReno / CUBIC)都是基于丢包的拥塞控制。它们把”丢包”作为”网络拥塞”的信号。这个假设有一个根本问题:

# 基于丢包的算法的问题:
#
# 1. 反馈延迟
#    丢包发生在路由器队列溢出时
#    但队列从空到满之间的过程中,延迟已经在增加了
#    基于丢包的算法在队列溢出之前"看不到"拥塞
#    → 它们总是"填满队列"后才反应
#
# 2. Bufferbloat 问题
#    现代路由器的队列很大(几百 ms 的缓冲)
#    基于丢包的算法会填满这些队列
#    → 延迟升高(队列延迟)但不丢包
#    → 算法认为"没有拥塞",继续增大 cwnd
#    → 实际延迟可能从 10ms 升到 200ms
#
# 3. 无线网络的随机丢包
#    无线网络的丢包不一定是拥塞导致的
#    可能是信号干扰、信道衰落
#    基于丢包的算法把这些"随机丢包"误判为拥塞
#    → 不必要地降低 cwnd → 吞吐量下降

# 基于延迟的替代方案:
# Vegas (1994): 通过 RTT 增长检测拥塞
# FAST TCP: 基于延迟的高速算法
# BBR (2016): Google 的基于带宽和延迟的算法
# → BBR 将在下一篇详细讲解

# 两种信号的对比:
# 
# 丢包信号:
#   优点:信号明确(丢了就是丢了)
#   缺点:反馈太迟、填满队列、无法区分随机丢包
#
# 延迟信号:
#   优点:早期反馈、减少 Bufferbloat
#   缺点:RTT 测量噪声大、在多流竞争中可能不公平

ECN:显式拥塞通知

ECN(Explicit Congestion Notification)是一种让路由器在丢包之前就通知发送方的机制:

# ECN 的工作原理:
# 1. TCP 握手时协商 ECN 支持(SYN 中的 ECE + CWR 标志)
# 2. 发送方在 IP 头中标记 ECT(0) 或 ECT(1)(ECN-Capable Transport)
# 3. 路由器队列接近满时,不丢包,而是将 ECT 标记改为 CE(Congestion Experienced)
# 4. 接收方在 TCP ACK 中设置 ECE(ECN-Echo)标志
# 5. 发送方收到 ECE → 像收到丢包信号一样降低 cwnd
# 6. 发送方在下一个数据包中设置 CWR(Congestion Window Reduced)通知接收方

# 检查系统是否支持 ECN
sysctl net.ipv4.tcp_ecn
# 0: 禁用
# 1: 启用(主动请求 ECN)
# 2: 被动支持(对方请求时接受,Linux 默认)

# 启用 ECN
sysctl -w net.ipv4.tcp_ecn=1

# 用 tcpdump 观察 ECN 标记
tcpdump -i eth0 -nn 'tcp[13] & 0xC0 != 0' -c 10
# 过滤 ECE 或 CWR 标志位被设置的包

# ECN 在 CUBIC 中的使用:
# CUBIC 收到 ECN-Echo 的反应和收到 3 个 dup ACK 一样
# ssthresh = cwnd × 0.7
# 但不触发快速重传(因为包没有真的丢)

# ECN 的部署现状:
# 服务器支持:~70%(被动支持, tcp_ecn=2)
# 主动使用:< 5%
# 主要障碍:中间设备可能丢弃带 ECN 标记的包

十、工程实践:选择和调优

何时需要更换拥塞控制算法

# 大多数场景下 CUBIC 是最佳选择——它是默认值是有原因的

# 考虑更换的场景:

# 1. 高延迟高带宽链路(跨洲际传输)
#    → 考虑 BBR(下一篇详解)
#    优势:不依赖丢包信号,在有 Bufferbloat 的网络中表现更好

# 2. 内部数据中心(低延迟, 受控环境)
#    → CUBIC 通常足够好
#    → DCTCP(Data Center TCP)可能更适合(基于 ECN)

# 3. 容器/K8s 环境
#    → 拥塞控制算法在宿主机内核设置
#    → Pod 不能独立修改(除非特权容器)
#    → 统一使用 CUBIC 或 BBR

# 4. 流媒体/实时传输
#    → 通常使用 UDP(不走 TCP 拥塞控制)
#    → 如果必须用 TCP, BBR 比 CUBIC 更适合低延迟需求

拥塞控制参数调优

# === initcwnd ===
# 增大初始窗口可以减少短连接的加载时间
ip route change default via 10.0.0.1 dev eth0 initcwnd 20
# 从默认 10 增到 20
# 对短小的 API 请求和网页资源最有效
# 风险:initcwnd 过大可能在高丢包率的网络中导致更多重传

# === ssthresh ===
# 通常不需要手动设置
# 但可以通过路由设置初始 ssthresh
ip route change default via 10.0.0.1 dev eth0 ssthresh 100
# 跳过慢启动的前期阶段

# === Pacing ===
# TCP Pacing 可以均匀发送数据,减少突发
# CUBIC 默认开启 pacing(在 4.20+ 内核中)
sysctl net.ipv4.tcp_pacing
# 查看 pacing 是否开启

# pacing 的好处:
# 不开启 pacing 时,cwnd 内的所有数据可能在收到 ACK 后瞬间发出
# → 微突发(micro-burst)→ 交换机队列瞬间溢出
# 开启 pacing 后,数据均匀分布在一个 RTT 内发送

# === FQ (Fair Queue) 调度器 ===
# 配合 pacing 使用的队列调度器
tc qdisc add dev eth0 root fq
# fq 会根据 TCP 的 pacing_rate 调度每条流的发送

常见问题诊断

# 问题 1:新建连接传输慢
# 症状:每次新 TCP 连接的前几个 RTT 传输量很小
# 原因:initcwnd 太小
# 诊断:
ss -ti | grep cwnd
# 新连接的 cwnd 如果很小(如 cwnd:2),说明 initcwnd 配置低
# 解决:
ip route change default via GW dev eth0 initcwnd 10

# 问题 2:长连接突然吞吐下降后恢复很慢
# 症状:丢包后吞吐量下降,恢复需要很长时间
# 原因:在高 BDP 网络中使用 Reno
# 诊断:
ss -ti | head -5
# 如果显示 reno 而不是 cubic → 换算法
sysctl -w net.ipv4.tcp_congestion_control=cubic

# 问题 3:cwnd 波动剧烈
# 症状:cwnd 反复大幅上升然后骤降
# 可能原因:
# a) 链路本身不稳定(如无线网络)
# b) 竞争流量突发
# c) 中间设备的 QoS 策略
# 诊断:
# 持续记录 cwnd 变化
while true; do
    ts=$(date +%s.%N)
    cwnd=$(ss -ti dst TARGET | grep -oP 'cwnd:\K[0-9]+')
    echo "$ts $cwnd"
    sleep 0.01
done | tee /tmp/cwnd_log.txt | tail -20
# 分析 cwnd 变化模式

十一、案例:跨洲际数据同步的拥塞控制选型

# 背景:
# 北京 ↔ 法兰克福的数据库同步
# 链路:专线 1Gbps, RTT 240ms
# 问题:同步吞吐量只有 ~50Mbps,远低于带宽容量

# === 第 1 步:计算理论限制 ===
# BDP = 1Gbps × 240ms = 30MB
# 需要 cwnd = 30MB / 1460 ≈ 20,548 个 MSS

# === 第 2 步:检查当前状态 ===
ss -ti dst FRANKFURT_IP
# cubic cwnd:86 ssthresh:78 rtt:240/2 mss:1460

# cwnd 只有 86(只利用了 86 × 1460 / 240ms ≈ 4.1Mbps 的带宽)
# 问题:cwnd 远小于 BDP 所需的 20,548

# === 第 3 步:检查限制因素 ===
# a) 流量控制限制?
ss -ti dst FRANKFURT_IP | grep rcv_space
# rcv_space:29200  ← 接收窗口只有 ~29KB!

# 找到了:接收窗口太小,限制了 cwnd 的增长
# 有效发送窗口 = min(rwnd, cwnd) = min(29KB, ...) ← 被 rwnd 限死

# b) 检查接收端配置
# 在法兰克福服务器上:
sysctl net.ipv4.tcp_rmem
# 4096 87380 6291456
sysctl net.core.rmem_max
# 212992  ← 只有 ~208KB,远小于 BDP

# === 第 4 步:修复 ===
# 两端都执行:
sysctl -w net.ipv4.tcp_rmem="4096 524288 67108864"
sysctl -w net.ipv4.tcp_wmem="4096 524288 67108864"
sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864

# 确认窗口缩放开启
sysctl net.ipv4.tcp_window_scaling
# 1  ← 已开启

# === 第 5 步:选择拥塞控制算法 ===
# 在高 BDP 链路上, CUBIC 比 Reno 更适合
# 但 BBR 可能更好(它不依赖丢包信号)
sysctl -w net.ipv4.tcp_congestion_control=cubic

# === 第 6 步:验证 ===
iperf3 -c FRANKFURT_IP -t 30 -i 5
# [  5]   0.00-5.00   sec   550 MBytes   923 Mbits/sec
# [  5]   5.00-10.00  sec   560 MBytes   939 Mbits/sec
# ...
# 吞吐量从 50Mbps 提升到 ~930Mbps

# 关键教训:
# 拥塞控制和流量控制是两个独立的限制
# 即使拥塞控制算法选对了,如果接收缓冲区太小
# cwnd 也无法增长到充分利用带宽

十二、结论

拥塞控制是 TCP 最复杂也最关键的子系统。从 Tahoe 到 CUBIC,30 年的演进可以总结为一条主线:如何更快、更准确地利用网络容量

几个核心认知:

  1. CUBIC 是大多数场景的正确选择。 它是 Linux 默认算法,在高 BDP 和低 BDP 网络中都有良好表现。除非你有明确的性能数据表明 CUBIC 不适合,否则不要随意更换。

  2. 拥塞控制和流量控制是两个独立的限制。 min(rwnd, cwnd) 意味着即使拥塞控制表现完美,流量控制的限制也可能使其失效。诊断吞吐量问题时两者都要检查。

  3. 基于丢包的算法有固有局限。 Tahoe / Reno / CUBIC 都把丢包当作拥塞信号。在 Bufferbloat 严重的网络或无线网络中,这会导致高延迟或不必要的降速。BBR 通过使用延迟和带宽信号来解决这个问题——这是下一篇的主题。

  4. initcwnd 对短连接影响巨大。 大多数 HTTP 请求在几个 RTT 内完成,根本来不及走完慢启动。增大 initcwnd 到 10-20 是最简单有效的优化。

  5. 永远用数据说话。 ss -ti 提供了 cwnd、ssthresh、rtt、delivery_rate 等所有关键指标。在改任何参数之前先看数据,改完之后用 iperf3 验证效果。

下一篇我们将深入 BBR——Google 提出的基于带宽和延迟的拥塞控制算法,一个完全不同的范式。


上一篇:TCP 流量控制:滑动窗口的工程细节

下一篇:BBR 深度剖析:基于带宽的拥塞控制革命

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-19 · network

【网络工程】BBR 深度剖析:基于带宽的拥塞控制革命

BBR 抛弃了 30 年来基于丢包的拥塞控制范式,改为直接估算瓶颈带宽和最小 RTT。本文剖析 BBR v1/v2/v3 的状态机与工程行为,分析 BBR 与 CUBIC 共存的公平性问题,并给出生产环境的部署与调优指南。

2025-07-30 · network

【网络工程】内核网络参数调优:sysctl 全景与实战

Linux 内核网络参数是系统网络性能的基础旋钮。本文从 /proc/sys/net/ 的参数体系出发,系统讲解收发缓冲区自动调优、TCP Backlog 队列、conntrack 连接追踪表、SYN Flood 防护参数、TIME_WAIT 管理,以及参数调优的系统化方法论——先基准、再调整、后验证。


By .