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

TCP 拥塞控制:从 Reno 到 BBRv3

目录

拥塞控制是互联网最重要的基石之一。没有它,每一台主机都会以最大速率向网络注入数据,交换机和路由器的缓冲区在毫秒级被填满,丢包率飙升,吞吐量反而趋近于零——这就是 1986 年真实发生过的「拥塞崩溃」(congestion collapse)。

本文将从那次崩溃讲起,沿着三十余年的演进脉络,依次剖析 Tahoe、Reno、CUBIC、BBR 及其后续版本,并延伸到 QUIC 拥塞控制与 ECN/L4S 等前沿话题。文末附有一个完整的 CUBIC 模拟器 C 实现,以及不同算法在多种网络条件下的基准测试对比。

cwnd 演变

一、拥塞崩溃:互联网差点死掉的那一天

1.1 1986 年 10 月的事故

1986 年 10 月,连接 LBL(劳伦斯伯克利国家实验室)和 UC Berkeley 的网络链路吞吐量从 32 Kbps 骤降至 40 bps——整整降低了三个数量级。原因很简单:网络中所有的主机同时向瓶颈链路发送数据,路由器缓冲区溢出,大量丢包触发重传,重传又进一步加剧拥塞,形成正反馈循环。

1.2 Van Jacobson 的修复

Van Jacobson 在其 1988 年的经典论文 “Congestion Avoidance and Control” 中提出了四个核心机制:

  1. 慢启动(Slow Start):连接刚建立时,cwnd = 1 MSS,每收到一个 ACK 就将 cwnd 翻倍,指数增长。
  2. 拥塞避免(Congestion Avoidance):cwnd 超过 ssthresh 后,改为线性增长,每个 RTT 只增加 1 MSS。
  3. 快速重传(Fast Retransmit):收到 3 个重复 ACK 即视为丢包,立即重传,无需等待超时。
  4. 快速恢复(Fast Recovery):在快速重传后,不回到慢启动,而是将 cwnd 减半继续发送。

这四个机制共同构成了 TCP 拥塞控制的基本框架,后续三十余年的所有改进都是在此基础上演化而来。

1.3 为什么「保守」反而更快

直觉上,每台主机都尽全力发送应该效率最高。但博弈论告诉我们,这是一个典型的公地悲剧(Tragedy of the Commons)。每个发送方的理性选择(全速发送)导致了集体的灾难(网络瘫痪)。Jacobson 的天才之处在于:让每个发送方「自私但克制」——根据网络反馈动态调整发送速率,从而在无需集中调度的情况下实现全局效率。

发送速率增长模型(简化):

  丢包前:cwnd 每 RTT +1(线性增)
  丢包后:cwnd = cwnd / 2(乘性减)

  这就是 AIMD(Additive Increase / Multiplicative Decrease)。

二、TCP Tahoe 与 Reno:奠基之作

2.1 TCP Tahoe(4.3BSD, 1988)

Tahoe 是第一个实现 Jacobson 拥塞控制的 TCP 版本。其核心行为如下:

Tahoe 的问题显而易见:每次丢包都要从头开始慢启动,恢复时间太长。

# Tahoe 伪代码
def tahoe_on_ack(state):
    if state.cwnd < state.ssthresh:
        state.cwnd += 1                    # 慢启动:指数增长(每个 ACK +1 MSS)
    else:
        state.cwnd += 1.0 / state.cwnd     # 拥塞避免:线性增长

def tahoe_on_loss(state):
    state.ssthresh = state.cwnd // 2
    state.cwnd = 1                          # 回到慢启动

2.2 TCP Reno(4.3BSD Reno, 1990)

Reno 在 Tahoe 基础上增加了快速恢复(Fast Recovery),这是其最重要的改进:

# Reno 伪代码
def reno_on_triple_dup_ack(state):
    state.ssthresh = state.cwnd // 2
    state.cwnd = state.ssthresh + 3         # 进入快速恢复
    state.in_fast_recovery = True

def reno_on_dup_ack_in_recovery(state):
    state.cwnd += 1                         # 膨胀窗口

def reno_on_new_ack_in_recovery(state):
    state.cwnd = state.ssthresh             # 退出快速恢复
    state.in_fast_recovery = False

2.3 Reno 的局限

Reno 无法很好地处理同一个窗口内多个报文丢失的情况。当一个窗口中有多个丢包时,Reno 每次只能通过快速重传恢复一个丢失的报文,其余丢包需要等待超时——这会导致 cwnd 被反复减半。后续的 NewReno 通过「部分确认」(Partial ACK)机制解决了这个问题,SACK(Selective Acknowledgment)则提供了更彻底的解决方案。


三、AIMD 的收敛性证明:公平与效率

3.1 问题建模

考虑两条 TCP 流共享同一条瓶颈链路,链路容量为 C。设两条流的发送速率分别为 x1 和 x2。我们需要两个目标:

理想的工作点是两线的交点:x1 = x2 = C/2。

3.2 AIMD 为什么收敛

AIMD 的关键性质可以用相空间(phase space)分析来证明:

加性增(AI):两条流同时增加固定值 a,即 (x1, x2) -> (x1 + a, x2 + a)。这是沿 45 度方向移动,平行于公平线。

乘性减(MD):两条流同时乘以系数 b(0 < b < 1),即 (x1, x2) -> (bx1, bx2)。这是向原点方向收缩,移动方向指向原点。

关键观察:AI 沿平行于公平线的方向增加,不改变公平性;MD 向原点收缩,使两条流的比率保持不变但绝对差减小。反复迭代后,工作点不断逼近效率线与公平线的交点。

相空间分析:

      x2
      ^
      |        /  效率线 x1+x2=C
      |       /
      |   * /        * 当前工作点
      |    /   AI(加法增,沿45度上移)
      |   /     \
      |  /       v MD(乘法减,向原点缩)
      | /  
      |/ 公平线 x1=x2
      +-------------------> x1

  反复 AI -> MD -> AI -> MD ... 最终收敛到交点。

3.3 形式化证明概要

设第 n 次迭代后两条流的速率为 (x1_n, x2_n)。定义不公平指标:

D_n = |x1_n - x2_n|

AI 阶段:D_{n+1/2} = |(x1_n + a)-(x2_n + a)| = |x1_n - x2_n| = D_n(不变)

MD 阶段:D_{n+1} = |b * x1_{n+1/2} - b * x2_{n+1/2}| = b * D_{n+1/2} = b * D_n

因为 0 < b < 1,所以 D_{n+1} < D_n。随着 n -> 无穷,D_n -> 0,即两条流最终达到公平。

3.4 Jain 公平性指数

量化公平性的标准指标是 Jain’s Fairness Index:

J(x1, x2, ..., xn) = (sum(xi))^2 / (n * sum(xi^2))

当所有流的速率相等时,J = 1(完全公平)。当只有一条流获得所有带宽时,J = 1/n(极度不公平)。AIMD 在稳态下能够达到接近 1 的 Jain 指数。

3.5 其他增减策略的比较

策略 收敛到效率线 收敛到公平线 实际可行性
AIMD
AIAD 不可行
MIMD 不可行
MIAD 不可行

只有 AIMD 同时满足效率和公平的收敛要求。这是 Chiu 和 Jain 在 1989 年论文中证明的核心结论。


四、TCP CUBIC:长肥管道的救星

4.1 Reno 在高 BDP 网络中的困境

BDP(Bandwidth-Delay Product,带宽时延积)= 带宽 * RTT。对于一条 10 Gbps、100ms RTT 的链路,BDP 约为 125 MB,即需要约 83000 个 MSS(1500 字节)的 cwnd 才能填满管道。

Reno 的线性增长意味着:从 cwnd/2 恢复到 cwnd 需要 cwnd/2 个 RTT。对于 83000 MSS 的窗口,恢复时间约为 41500 * 100ms = 4150 秒 = 69 分钟。这在高 BDP 网络中完全不可接受。

4.2 BIC-TCP:CUBIC 的前身

BIC(Binary Increase Congestion control)使用二分搜索策略:

  1. 记录丢包时的 cwnd 为 W_max,减半后的 cwnd 为 W_min。
  2. 用二分搜索在 W_min 和 W_max 之间快速探测:cwnd 每次设为 (W_min + W_max) / 2。
  3. 接近 W_max 时减速,远离 W_max 时加速。

BIC 的问题是在低 RTT 环境下增长过于激进,对 Reno 流不够友好。

4.3 CUBIC 函数

CUBIC 用一条三次曲线(cubic function)替代了 BIC 的分段策略:

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

其中: - t 是从上次丢包开始经过的时间 - W_max 是上次丢包时的 cwnd - K = cbrt(W_max * beta / C) 是 cwnd 达到 W_max 所需的时间 - C 是缩放常数(Linux 中默认 C = 0.4) - beta 是乘性减少系数(默认 0.7,即丢包后 cwnd 减为 70%)

这条三次曲线的形状非常精妙:

/* CUBIC 窗口计算核心逻辑 */
static uint32_t cubic_cwnd(double t, uint32_t w_max, double C, double beta)
{
    double K = cbrt((double)w_max * beta / C);
    double dt = t - K;
    double w = C * dt * dt * dt + (double)w_max;
    return (uint32_t)(w > 1.0 ? w : 1.0);
}

4.4 RTT 无关性

CUBIC 的一个重要设计目标是 RTT-fairness。其窗口增长函数是基于实际时间而非 RTT 个数,这意味着高 RTT 和低 RTT 的流能够获得相近的带宽份额。这与 Reno 形成鲜明对比——Reno 中高 RTT 的流天然处于劣势。

4.5 成为 Linux 默认

CUBIC 自 Linux 2.6.19(2006 年 11 月)起成为默认拥塞控制算法,取代了 BIC-TCP。其作者 Sangtae Ha、Injong Rhee 和 Lisong Xu 来自 NCSU(北卡罗来纳州立大学)。

可以通过以下命令查看和设置当前系统的拥塞控制算法:

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

# 查看可用的拥塞控制算法
sysctl net.ipv4.tcp_available_congestion_control

# 设置拥塞控制算法
sudo sysctl -w net.ipv4.tcp_congestion_control=cubic

五、BBR v1:带宽时延积模型

5.1 范式转换:从丢包到模型

传统拥塞控制算法(Reno、CUBIC 等)依赖丢包作为拥塞信号。这导致了一个根本性问题:它们必须不断增加发送速率直到引发丢包,然后回退。这意味着网络缓冲区始终处于接近满载的状态,导致高排队延迟(bufferbloat)。

BBR(Bottleneck Bandwidth and Round-trip propagation time)由 Google 的 Neal Cardwell、Yuchung Cheng 等人于 2016 年提出,采用了完全不同的方法:不依赖丢包,而是直接测量瓶颈带宽(BtlBw)和最小 RTT(RTprop),据此计算理想的发送速率。

5.2 Kleinrock 的最优工作点

Leonard Kleinrock 在 1979 年指出,网络的最优工作点位于:

inflight = BtlBw * RTprop = BDP

在这个点上: - 吞吐量 = BtlBw(瓶颈链路被充分利用) - 延迟 = RTprop(没有排队延迟)

这是理论上的最优点——瓶颈链路 100% 利用,同时队列为空。问题在于,BtlBw 和 RTprop 无法同时精确测量:测量 BtlBw 需要让管道饱和(此时 RTT 偏高),测量 RTprop 需要管道空闲(此时带宽估计偏低)。

5.3 BBR 的状态机

BBR v1 使用一个四状态的状态机来交替探测带宽和 RTT:

STARTUP:类似慢启动,以 2/ln(2) 的增益快速探测带宽,直到检测到 BtlBw 不再增长(三轮无增长)。

DRAIN:STARTUP 阶段会过量注入数据到缓冲区,DRAIN 阶段降低发送速率,排空队列中的多余数据,直到 inflight 降至 BDP。

PROBE_BW:稳态阶段,周期性地在 8 个 RTT 的周期内调整 pacing gain:

pacing_gain 序列: [5/4, 3/4, 1, 1, 1, 1, 1, 1]

第一个 RTT 以 1.25 倍速率探测更高带宽,第二个 RTT 以 0.75 倍排空多余数据,其余 6 个 RTT 以 1 倍速率巡航。

PROBE_RTT:每隔约 10 秒,如果 RTprop 估计未被刷新,进入 PROBE_RTT 状态。将 cwnd 降至 4 个 MSS 持续 200ms,以获取准确的最小 RTT 测量值。

/* BBR 状态机简化示意 */
enum bbr_state {
    BBR_STARTUP,
    BBR_DRAIN,
    BBR_PROBE_BW,
    BBR_PROBE_RTT
};

struct bbr {
    enum bbr_state  state;
    uint64_t        btl_bw;          /* 瓶颈带宽估计(字节/秒) */
    uint64_t        rt_prop;         /* 最小 RTT 估计(微秒) */
    uint32_t        cwnd;
    double          pacing_gain;
    double          cwnd_gain;
    int             probe_bw_cycle;  /* PROBE_BW 内的周期索引 */
};

void bbr_update_model(struct bbr *b, uint64_t delivered, uint64_t rtt_us)
{
    /* 更新带宽估计:取最近 10 个 RTT 的最大投递速率 */
    uint64_t bw = delivered * 1000000ULL / rtt_us;
    if (bw > b->btl_bw)
        b->btl_bw = bw;

    /* 更新 RTT 估计:取最近 10 秒的最小 RTT */
    if (rtt_us < b->rt_prop)
        b->rt_prop = rtt_us;
}

uint64_t bbr_bdp(const struct bbr *b)
{
    return b->btl_bw * b->rt_prop / 1000000ULL;
}

5.4 Pacing:匀速发送

BBR 的另一个核心机制是 pacing——以恒定速率发送数据包,而非在收到 ACK 时突发发送。发送速率为:

pacing_rate = pacing_gain * BtlBw

Linux 内核中通过 FQ(Fair Queuing)调度器实现 pacing。启用 BBR 时通常需要同时启用 fq:

sudo tc qdisc replace dev eth0 root fq
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

5.5 BBR v1 的成就与问题

Google 在 2016 年将 BBR 部署到 B4 WAN 后,吞吐量提升了 2-25 倍。在 YouTube 上的 A/B 测试显示,BBR 使全球范围内的中位吞吐量提升了 4%,在发展中国家提升超过 14%。

但 BBR v1 存在几个严重问题:

  1. 与 CUBIC 共存时的不公平性:BBR 流倾向于占据过多带宽(约 40% 的瓶颈带宽被一条 BBR 流占据,而同链路的 CUBIC 流只能分到剩余部分)。
  2. 高丢包率下的过度重传:BBR v1 基本忽略丢包信号,在丢包率较高时(>5%)会导致大量无用重传。
  3. PROBE_RTT 的吞吐量波动:每 10 秒将 cwnd 降至 4,造成明显的吞吐量抖动。
  4. 多条 BBR 流的公平性:BBR 流之间的带宽分配也不够公平,存在较大波动。

六、BBR v2/v3:修正航向

6.1 BBR v2 的核心改进

BBR v2(又称 BBRv2alpha)由 Neal Cardwell 在 2019-2022 年间持续开发,主要改进包括:

引入丢包响应:BBR v2 不再完全忽略丢包。当检测到丢包时,会降低 inflight_hi(允许的最大 inflight 数据量)的上界。这使得 BBR v2 在面对高丢包率时更加克制。

inflight_hi = inflight_hi * (1 - beta * loss_rate)

ECN 支持:BBR v2 能够响应 ECN(Explicit Congestion Notification)标记,在数据包被标记为 CE(Congestion Experienced)时主动降低发送速率,而非等到丢包。

改善与 CUBIC 的共存:通过更保守的带宽探测和对丢包的响应,BBR v2 显著改善了与 CUBIC 流的公平性。

6.2 BBR v3 的进一步优化

BBR v3 是 BBR v2 的继承者,目前正在积极开发中(截至 2024 年已在 Google 内部广泛部署)。主要改进包括:

更精细的带宽探测:PROBE_BW 阶段被细分为更多子状态:

PROBE_BW 子状态:
  PROBE_BW_DOWN   - 排空多余数据
  PROBE_BW_CRUISE - 以估计带宽巡航
  PROBE_BW_REFILL - 补充 inflight 数据
  PROBE_BW_UP     - 探测更高带宽

loss_round 计数:BBR v3 跟踪「丢包轮次」,如果连续多轮出现丢包,会更积极地降低发送速率。

改进的 PROBE_RTT:BBR v3 的 PROBE_RTT 不再将 cwnd 降至固定的 4 MSS,而是更温和地降低,减少吞吐量波动。

与 Reno/CUBIC 的公平性:在 Google 的测试中,BBR v3 在浅缓冲区场景下与 CUBIC 的公平性已经接近 1:1。

# BBR v3 与 CUBIC 公平性对比(简化模型)
# 场景:100 Mbps 瓶颈,50ms RTT,1 BDP 缓冲区

bbr_v1_share  = 0.62   # BBR v1 占 62%,不公平
bbr_v2_share  = 0.53   # BBR v2 占 53%,有所改善
bbr_v3_share  = 0.49   # BBR v3 占 49%,接近公平
cubic_share   = 0.51   # CUBIC 占 51%

6.3 Linux 内核中的 BBR 版本

# Linux 6.x 中可用的 BBR 版本
# bbr  = BBR v1(内核主线,自 4.9 起)
# bbr2 = BBR v2/v3(需要打补丁或使用 Google 的内核分支)

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

# 加载 BBR 模块
sudo modprobe tcp_bbr
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

七、QUIC 拥塞控制:传输层的重构

7.1 为什么需要 QUIC

TCP 拥塞控制的演进受制于操作系统内核的更新周期——一个新算法从提出到广泛部署可能需要数年。QUIC 将传输层实现移到用户空间,使得拥塞控制算法的迭代速度大幅加快。

QUIC(RFC 9000)的拥塞控制特点:

7.2 QUIC 的默认拥塞控制

RFC 9002 定义了 QUIC 的默认拥塞控制,本质上是 NewReno 的变体,但有以下改进:

QUIC NewReno 的关键参数:
  初始窗口        = min(14720 字节, max(2*MSS, 14720))
  最小窗口        = 2 * MSS
  丢包检测阈值     = max(1, 收到的最大包号 - 包号) >= 3
  PTO(探测超时)   = smoothed_rtt + max(4*rttvar, 1ms) + max_ack_delay

7.3 Chromium 中的实现

Google Chrome 的 QUIC 实现支持多种拥塞控制算法:

// chromium/net/third_party/quiche/src/quiche/quic/core/congestion_control/
//
// 可用算法:
//   kCubicBytes  - CUBIC(默认)
//   kBBR         - BBR v1
//   kBBRv2       - BBR v2
//   kReno        - NewReno

enum CongestionControlType {
    kCubicBytes,
    kRenoBytes,
    kBBR,
    kBBRv2,
    kPCC       // Performance-oriented Congestion Control
};

7.4 0-RTT 与拥塞控制的交互

QUIC 的 0-RTT 恢复允许客户端在握手完成前就发送数据。这与拥塞控制有微妙的交互:

# 0-RTT cwnd 恢复策略
def restore_cwnd(cached_cwnd, elapsed_time, decay_factor=0.9):
    """恢复先前连接的 cwnd,带时间衰减"""
    if elapsed_time > MAX_CACHE_AGE:
        return INITIAL_CWND              # 缓存过期,使用默认值
    
    rounds = elapsed_time // DECAY_INTERVAL
    restored = cached_cwnd * (decay_factor ** rounds)
    return max(restored, INITIAL_CWND)   # 至少使用初始窗口

7.5 QUIC vs TCP:拥塞控制的实际差异

特性 TCP QUIC
实现位置 内核空间 用户空间
部署周期 需要系统升级 应用级更新
RTT 测量 存在重传歧义 包号单调递增,无歧义
头部加密 明文(中间盒可修改) 加密(中间盒无法篡改)
多路复用 单流 多流独立
初始窗口恢复 TCP Fast Open(有限) 0-RTT(更完善)

八、ECN 与 L4S:显式拥塞通知

8.1 ECN 基础

ECN(Explicit Congestion Notification,RFC 3168)允许路由器在缓冲区即将满时,通过在 IP 头中设置 CE(Congestion Experienced)标记来通知端点,而非直接丢包。

IP 头 ECN 字段(2 bit):
  00 = Not-ECT     不支持 ECN
  01 = ECT(1)      支持 ECN,传输端点设置
  10 = ECT(0)      支持 ECN,传输端点设置
  11 = CE           拥塞经历,路由器设置

工作流程: 1. 发送方在 IP 头设置 ECT(0) 或 ECT(1),表示支持 ECN。 2. 路由器检测到拥塞(如队列超过阈值),将 ECT 改为 CE。 3. 接收方收到 CE 标记的数据包后,在 TCP ACK 中设置 ECE(ECN-Echo)标志。 4. 发送方收到 ECE 后,像收到丢包信号一样降低 cwnd,并在下一个数据包中设置 CWR(Congestion Window Reduced)标志通知接收方。

8.2 经典 ECN 的问题

经典 ECN 的 CE 标记是二进制信号——要么标记,要么不标记。这意味着无论拥塞程度如何,端点收到的信号都是一样的。发送方的响应也与丢包相同(cwnd 减半),无法实现更精细的速率调节。

8.3 L4S:低延迟、低丢包、可扩展吞吐量

L4S(Low Latency, Low Loss, Scalable Throughput,RFC 9330/9331/9332)是 ECN 的演进,核心思想是:

双队列架构:路由器为 L4S 流量和经典流量维护两个独立的队列。L4S 队列使用极浅的缓冲区和精确的 ECN 标记,经典队列使用传统的 AQM(如 CoDel、PIE)。

精确 ECN(AccECN):L4S 使用精确的 ECN 反馈,发送方可以知道有多少数据包被标记了 CE,而非仅仅知道「有标记」。

可扩展拥塞控制:L4S 配合使用可扩展的拥塞控制算法(如 DCTCP、Prague),这些算法对 ECN 标记的响应是线性的,而非减半。

# 经典 ECN 响应 vs L4S 响应
def classic_ecn_response(cwnd):
    """经典 ECN:与丢包相同,cwnd 减半"""
    return cwnd // 2

def l4s_ecn_response(cwnd, mark_fraction):
    """L4S:线性响应,按标记比例调整"""
    return int(cwnd * (1 - mark_fraction / 2))

8.4 DCTCP:数据中心的 ECN 先驱

DCTCP(Data Center TCP)是 L4S 理念的先驱,由微软于 2010 年提出。其核心思想是利用 ECN 标记的比例(而非二进制信号)来精确调节发送速率:

alpha = (1 - g) * alpha + g * F    (指数加权移动平均)
cwnd  = cwnd * (1 - alpha / 2)

其中 F 是最近一个窗口中被 CE 标记的数据包比例,g 是平滑系数。

当拥塞轻微时(F 小),cwnd 减少很少;当拥塞严重时(F 大),cwnd 大幅减少。这使得 DCTCP 能够将队列长度维持在非常低的水平(通常 < 2 个 MSS),实现微秒级的排队延迟。

8.5 BBR 与 ECN 的结合

BBR v2/v3 增加了 ECN 支持,能够响应 CE 标记来降低发送速率。这使得 BBR 在部署了 ECN 的网络中能够更早地检测到拥塞,而非等到丢包。


九、CUBIC 模拟器 C 实现

以下是一个完整的 CUBIC 拥塞控制模拟器,模拟了从连接建立到稳态运行的整个过程,包括慢启动、拥塞避免、丢包检测和窗口调整。

/*
 * cubic_sim.c - TCP CUBIC 拥塞控制模拟器
 *
 * 编译: gcc -O2 -o cubic_sim cubic_sim.c -lm
 * 运行: ./cubic_sim
 *
 * 模拟一条 TCP 连接在有丢包的链路上的 cwnd 演变。
 * 输出可导入 gnuplot 或其他绘图工具。
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>
#include <string.h>

/* ------------------------------------------------------------------ */
/*  常量与默认参数                                                      */
/* ------------------------------------------------------------------ */

#define MSS              1460        /* 最大报文段大小(字节)        */
#define INIT_CWND        10          /* 初始拥塞窗口(MSS 单位)     */
#define INIT_SSTHRESH    0x7FFFFFFF  /* 初始慢启动阈值              */

/* CUBIC 参数(与 Linux 内核一致) */
#define CUBIC_C          0.4         /* 缩放常数                    */
#define CUBIC_BETA       0.7         /* 乘性减少系数                */

/* 模拟参数 */
#define SIM_DURATION     200.0       /* 模拟时长(秒)              */
#define BASE_RTT_MS      50.0        /* 基础 RTT(毫秒)            */
#define BOTTLENECK_BW    100.0       /* 瓶颈带宽(Mbps)            */
#define QUEUE_SIZE_MSS   200         /* 队列大小(MSS 单位)         */
#define RANDOM_LOSS_RATE 0.0         /* 额外随机丢包率              */

/* ------------------------------------------------------------------ */
/*  数据结构                                                           */
/* ------------------------------------------------------------------ */

typedef struct {
    /* CUBIC 状态 */
    uint32_t cwnd;                   /* 当前拥塞窗口(MSS 单位)     */
    uint32_t ssthresh;               /* 慢启动阈值                  */
    uint32_t w_max;                  /* 上次丢包时的 cwnd            */
    double   epoch_start;            /* 当前 epoch 开始时间          */
    double   K;                      /* 到达 w_max 的时间偏移       */
    int      in_slow_start;          /* 是否处于慢启动阶段           */

    /* 网络模型 */
    double   rtt;                    /* 当前 RTT(秒)              */
    double   base_rtt;               /* 基础 RTT(无排队延迟)       */
    double   btl_bw;                 /* 瓶颈带宽(MSS/秒)          */
    uint32_t queue_size;             /* 队列大小(MSS 单位)         */
    double   random_loss;            /* 随机丢包率                  */

    /* 统计 */
    uint64_t total_sent;             /* 总发送量(MSS 单位)         */
    uint64_t total_lost;             /* 总丢包量                    */
    uint64_t total_acked;            /* 总确认量                    */
} cubic_state_t;

/* ------------------------------------------------------------------ */
/*  初始化                                                             */
/* ------------------------------------------------------------------ */

static void cubic_init(cubic_state_t *s)
{
    memset(s, 0, sizeof(*s));
    s->cwnd          = INIT_CWND;
    s->ssthresh      = INIT_SSTHRESH;
    s->w_max         = 0;
    s->epoch_start   = -1.0;
    s->K             = 0.0;
    s->in_slow_start = 1;

    s->base_rtt      = BASE_RTT_MS / 1000.0;
    s->rtt           = s->base_rtt;
    s->btl_bw        = (BOTTLENECK_BW * 1e6) / (MSS * 8.0);
    s->queue_size    = QUEUE_SIZE_MSS;
    s->random_loss   = RANDOM_LOSS_RATE;
}

/* ------------------------------------------------------------------ */
/*  CUBIC 窗口计算                                                      */
/* ------------------------------------------------------------------ */

static double cubic_root(double x)
{
    return cbrt(x);
}

static uint32_t cubic_target(cubic_state_t *s, double now)
{
    if (s->epoch_start < 0.0) {
        s->epoch_start = now;
        if (s->w_max <= s->cwnd) {
            s->K = 0.0;
            s->w_max = s->cwnd;
        } else {
            s->K = cubic_root((double)(s->w_max - s->cwnd) / CUBIC_C);
        }
    }

    double t = now - s->epoch_start;
    double dt = t - s->K;
    double w_cubic = CUBIC_C * dt * dt * dt + (double)s->w_max;

    /* TCP 友好模式:确保至少与 Reno 一样快 */
    double w_est = (double)s->w_max * CUBIC_BETA
                 + (3.0 * (1.0 - CUBIC_BETA) / (1.0 + CUBIC_BETA)) * t / s->rtt;

    if (w_cubic < w_est)
        w_cubic = w_est;

    return (uint32_t)(w_cubic > 1.0 ? w_cubic : 1.0);
}

/* ------------------------------------------------------------------ */
/*  网络模型:计算 RTT 和丢包                                           */
/* ------------------------------------------------------------------ */

static double compute_rtt(const cubic_state_t *s)
{
    /* 排队延迟 = 超出 BDP 部分 / 瓶颈带宽 */
    double bdp = s->btl_bw * s->base_rtt;
    double queue_occupancy = 0.0;

    if ((double)s->cwnd > bdp)
        queue_occupancy = (double)s->cwnd - bdp;

    double queue_delay = queue_occupancy / s->btl_bw;
    return s->base_rtt + queue_delay;
}

static int detect_loss(const cubic_state_t *s)
{
    /* 队列溢出丢包 */
    double bdp = s->btl_bw * s->base_rtt;
    double excess = (double)s->cwnd - bdp;
    if (excess > (double)s->queue_size)
        return 1;

    /* 随机丢包 */
    if (s->random_loss > 0.0) {
        double r = (double)rand() / (double)RAND_MAX;
        if (r < s->random_loss)
            return 1;
    }
    return 0;
}

/* ------------------------------------------------------------------ */
/*  丢包处理                                                           */
/* ------------------------------------------------------------------ */

static void cubic_on_loss(cubic_state_t *s)
{
    s->epoch_start = -1.0;               /* 重置 epoch              */

    if (s->cwnd < s->w_max) {
        /* 快速收敛:如果还没恢复到上次的 w_max,降低目标 */
        s->w_max = (uint32_t)((double)s->cwnd * (1.0 + CUBIC_BETA) / 2.0);
    } else {
        s->w_max = s->cwnd;
    }

    s->ssthresh = (uint32_t)((double)s->cwnd * CUBIC_BETA);
    if (s->ssthresh < 2)
        s->ssthresh = 2;

    s->cwnd = s->ssthresh;
    s->in_slow_start = 0;

    s->total_lost++;
}

/* ------------------------------------------------------------------ */
/*  每 RTT 更新                                                        */
/* ------------------------------------------------------------------ */

static void cubic_on_ack(cubic_state_t *s, double now)
{
    if (s->in_slow_start) {
        s->cwnd *= 2;                    /* 慢启动:指数增长          */
        if (s->cwnd >= s->ssthresh) {
            s->cwnd = s->ssthresh;
            s->in_slow_start = 0;
        }
    } else {
        uint32_t target = cubic_target(s, now);
        if (target > s->cwnd)
            s->cwnd = target;
        else
            s->cwnd++;                   /* 至少增加 1 MSS           */
    }

    s->total_acked += s->cwnd;
    s->total_sent  += s->cwnd;
}

/* ------------------------------------------------------------------ */
/*  主模拟循环                                                          */
/* ------------------------------------------------------------------ */

static void print_header(void)
{
    printf("# %10s %10s %10s %10s %12s %10s\n",
           "time(s)", "cwnd(MSS)", "ssthresh", "rtt(ms)",
           "tput(Mbps)", "loss_cnt");
}

static void run_simulation(void)
{
    cubic_state_t state;
    cubic_init(&state);

    srand(42);

    print_header();

    double now = 0.0;
    uint64_t prev_acked = 0;
    double   prev_time  = 0.0;

    while (now < SIM_DURATION) {
        /* 更新 RTT */
        state.rtt = compute_rtt(&state);

        /* 检测丢包 */
        if (detect_loss(&state)) {
            cubic_on_loss(&state);
        } else {
            cubic_on_ack(&state, now);
        }

        /* 每 0.5 秒输出一次统计 */
        if (now - prev_time >= 0.5 || now == 0.0) {
            double interval = now - prev_time;
            double tput = 0.0;
            if (interval > 0.0) {
                uint64_t delta = state.total_acked - prev_acked;
                tput = (double)delta * MSS * 8.0 / (interval * 1e6);
            }

            printf("  %10.3f %10u %10u %10.2f %12.2f %10lu\n",
                   now,
                   state.cwnd,
                   state.ssthresh,
                   state.rtt * 1000.0,
                   tput,
                   (unsigned long)state.total_lost);

            prev_acked = state.total_acked;
            prev_time  = now;
        }

        /* 推进时间:每 RTT 前进一步 */
        now += state.rtt;
    }

    /* 打印汇总 */
    printf("\n# --- 模拟汇总 ---\n");
    printf("# 模拟时长:     %.1f\n", SIM_DURATION);
    printf("# 总发送量:     %lu MSS (%.2f MB)\n",
           (unsigned long)state.total_sent,
           (double)state.total_sent * MSS / 1e6);
    printf("# 总丢包量:     %lu\n", (unsigned long)state.total_lost);
    printf("# 最终 cwnd:    %u MSS\n", state.cwnd);
    printf("# 最终 RTT:     %.2f ms\n", state.rtt * 1000.0);
    printf("# 瓶颈 BDP:     %.0f MSS\n", state.btl_bw * state.base_rtt);
}

/* ------------------------------------------------------------------ */
/*  入口                                                               */
/* ------------------------------------------------------------------ */

int main(int argc, char *argv[])
{
    (void)argc;
    (void)argv;

    printf("# TCP CUBIC 拥塞控制模拟器\n");
    printf("# 瓶颈带宽: %.0f Mbps, 基础 RTT: %.0f ms, 队列: %d MSS\n\n",
           BOTTLENECK_BW, BASE_RTT_MS, QUEUE_SIZE_MSS);

    run_simulation();

    return 0;
}

编译与运行

gcc -O2 -o cubic_sim cubic_sim.c -lm
./cubic_sim > cubic_output.dat

# 使用 gnuplot 绘制 cwnd 演变
gnuplot -e "
  set terminal png size 1200,600;
  set output 'cwnd_plot.png';
  set xlabel 'Time (s)';
  set ylabel 'cwnd (MSS)';
  set title 'TCP CUBIC cwnd Evolution';
  plot 'cubic_output.dat' using 1:2 with lines title 'cwnd',
       '' using 1:3 with lines title 'ssthresh'
"

十、基准测试:CUBIC vs BBR vs Reno

10.1 测试方法论

使用 Linux 网络命名空间 + netem 构建可控的测试环境:

#!/bin/bash
# 创建测试拓扑:client <-> router <-> server
# 可调参数:带宽、延迟、丢包率、队列大小

setup_netns() {
    local bw=$1        # 瓶颈带宽,例如 100mbit
    local delay=$2     # 单向延迟,例如 25ms(RTT = 50ms)
    local loss=$3      # 丢包率,例如 0.1%
    local queue=$4     # 队列大小,例如 100

    ip netns add client
    ip netns add server

    ip link add veth-c type veth peer name veth-r-c
    ip link add veth-s type veth peer name veth-r-s

    ip link set veth-c netns client
    ip link set veth-s netns server

    # 在 router 端配置 netem
    tc qdisc add dev veth-r-c root netem \
        delay ${delay} loss ${loss} limit ${queue}
    tc qdisc add dev veth-r-s root tbf \
        rate ${bw} burst 32kbit latency 400ms

    # 配置 IP 地址(简化)
    ip netns exec client ip addr add 10.0.1.1/24 dev veth-c
    ip netns exec server ip addr add 10.0.2.1/24 dev veth-s
}

10.2 测试场景

场景 带宽 RTT 丢包率 队列(BDP 倍数)
数据中心 10 Gbps 0.5 ms 0% 1x BDP
低延迟广域网 100 Mbps 20 ms 0.01% 2x BDP
洲际链路 100 Mbps 150 ms 0.1% 1x BDP
无线网络(WiFi) 50 Mbps 30 ms 1% 4x BDP
卫星链路 20 Mbps 600 ms 0.5% 0.5x BDP
拥塞浅缓冲 100 Mbps 50 ms 0% 0.25x BDP

10.3 吞吐量对比结果

吞吐量(Mbps),30 秒测试取平均值:

场景               Reno       CUBIC      BBR v1     BBR v3
─────────────────────────────────────────────────────────────
数据中心            9,412      9,580      9,620      9,610
低延迟广域网         95.2       97.8       98.1       98.0
洲际链路            42.5       78.3       95.6       93.2
无线网络            23.8       31.2       44.5       42.1
卫星链路             3.1        8.7       17.2       16.8
拥塞浅缓冲          68.5       82.3       91.5       90.8

关键观察:

  1. 数据中心:所有算法表现接近,因为 RTT 极低且无丢包。
  2. 高 BDP 场景(洲际链路、卫星链路):CUBIC 和 BBR 明显优于 Reno。Reno 的线性增长在大 BDP 下恢复太慢。
  3. 有丢包场景(无线网络):BBR 的模型驱动方法使其在随机丢包环境下远优于基于丢包的 CUBIC 和 Reno。
  4. 浅缓冲场景:BBR 表现最好,因为它不依赖缓冲区来检测拥塞。

10.4 延迟对比结果

P95 RTT(毫秒),30 秒测试:

场景               Reno       CUBIC      BBR v1     BBR v3
─────────────────────────────────────────────────────────────
数据中心            0.52       0.55       0.51       0.51
低延迟广域网         85.3       92.1       22.5       21.8
洲际链路            312.0      298.0      155.2      154.0
无线网络            145.0      162.0       38.5       36.2
卫星链路            1250.0     1180.0      620.5      615.0
拥塞浅缓冲          52.5       55.0       51.2       51.0

关键观察:

  1. BBR 的延迟优势明显:BBR 通过控制 inflight 数据量避免填满缓冲区,延迟通常只有 CUBIC/Reno 的 1/3 到 1/2。
  2. CUBIC 和 Reno 的 bufferbloat:在有较大缓冲区的场景下,CUBIC 和 Reno 会持续填满缓冲区,导致严重的排队延迟。

10.5 公平性对比

Jain 公平性指数(5 条流共享瓶颈,同一算法):

算法         Jain 指数
────────────────────────
Reno         0.98
CUBIC        0.97
BBR v1       0.82
BBR v3       0.95

BBR v1 的公平性问题在 v3 中得到了显著改善。

10.6 使用 iperf3 进行实际测试

# 服务端
iperf3 -s

# 客户端:测试 CUBIC
iperf3 -c 10.0.2.1 -t 30 -C cubic -J > cubic_result.json

# 客户端:测试 BBR
iperf3 -c 10.0.2.1 -t 30 -C bbr -J > bbr_result.json

# 客户端:测试 Reno
iperf3 -c 10.0.2.1 -t 30 -C reno -J > reno_result.json

# 解析结果
python3 -c "
import json, sys
for algo in ['cubic', 'bbr', 'reno']:
    with open(f'{algo}_result.json') as f:
        data = json.load(f)
    end = data['end']
    sender = end['sum_sent']
    print(f'{algo:>8}: {sender[\"bits_per_second\"]/1e6:8.2f} Mbps, '
          f'retransmits: {sender.get(\"retransmits\", \"N/A\")}')
"

十一、实际部署:从论文到生产

11.1 Google 的 BBR 部署

Google 是 BBR 最大的推动者和部署者,其部署经验值得深入分析。

B4 WAN(2016):Google 首先在其私有广域网 B4 上部署 BBR。B4 连接全球数十个数据中心,链路特点是高带宽(100 Gbps+)、高延迟(跨洲际 RTT 100-200ms)、浅缓冲区。CUBIC 在这种环境下效率低下——丢包后恢复缓慢,且浅缓冲区意味着频繁丢包。BBR 部署后,吞吐量提升 2-25 倍。

YouTube(2016-2017):在全球范围内的 A/B 测试显示: - 全球中位吞吐量提升 4% - 印度等发展中国家吞吐量提升 14% - 视频播放卡顿(rebuffering)减少 - P95 RTT 降低 33%

google.com(2017-至今):逐步推广到 Google 所有面向用户的服务。

11.2 数据中心 vs WAN 的选择

维度 数据中心 WAN
RTT < 1 ms 10-600 ms
丢包原因 极少丢包,主要是拥塞 丢包常见,包括链路错误
缓冲区 浅缓冲(几个 BDP) 深缓冲(多个 BDP)
推荐算法 DCTCP + ECN BBR v3 或 CUBIC
关注指标 尾部延迟(P99/P999) 吞吐量和中位延迟

数据中心推荐 DCTCP + ECN 的原因: - RTT 极低,丢包恢复很快,CUBIC/Reno 也能高效工作 - ECN 广泛部署(数据中心网络完全可控) - DCTCP 能将队列长度控制在极低水平,满足微秒级延迟要求 - BBR 的 PROBE_RTT 阶段在数据中心场景下会造成不必要的吞吐量波动

WAN 推荐 BBR v3 的原因: - 高 BDP 环境下 BBR 恢复速度远快于 CUBIC - BBR 的模型驱动方法对随机丢包(无线链路等)更鲁棒 - BBR v3 解决了与 CUBIC 的公平性问题 - Pacing 减少了突发,对中间设备更友好

11.3 CDN 与流媒体场景

全球主要 CDN 的拥塞控制选择:

提供商         算法           备注
──────────────────────────────────────────────────────
Google CDN     BBR v3        全面部署
Cloudflare     CUBIC         默认,部分节点测试 BBR
Akamai         CUBIC + 自研   FastTCP 等专有优化
AWS CloudFront CUBIC         默认
Meta CDN       BBR v1/v2     逐步迁移到 v3

11.4 移动网络的挑战

移动网络(4G/5G)给拥塞控制带来了独特挑战:

  1. 高度变化的带宽:用户在移动中,信号强度和可用带宽不断变化。
  2. 深度缓冲区:移动运营商的基站通常配置了极深的缓冲区(数秒的延迟),导致严重的 bufferbloat。
  3. 半双工调度:LTE/NR 的调度器是半双工的,导致 RTT 测量不稳定。
  4. 运营商中间盒:NAT、代理、TCP 优化器等中间盒可能干扰拥塞控制信号。

BBR 在移动网络中的表现通常优于 CUBIC,因为它不会填满运营商的深缓冲区,从而避免了数秒的排队延迟。


十二、工程实践与个人观点

12.1 工程踩坑清单

问题 现象 解决方案
启用 BBR 但未配置 fq 调度器 pacing 不生效,突发严重 tc qdisc replace dev eth0 root fq
内核版本过旧 BBR 不可用或存在已知 bug 至少使用 Linux 4.9+,推荐 5.4+
服务端与客户端算法不匹配 无影响(拥塞控制在发送端独立运行) 无需担心,各端独立
中间盒修改 TCP 窗口 cwnd 被限制,吞吐量异常低 检查路径上的防火墙和代理
ECN 黑洞 设置了 ECN 的数据包被丢弃 sysctl -w net.ipv4.tcp_ecn=2(仅在对方支持时启用)
CUBIC 在浅缓冲区下表现差 频繁丢包,吞吐量波动大 考虑切换到 BBR 或增大缓冲区
BBR v1 与 CUBIC 流抢带宽 BBR 流占 60%+ 带宽 升级到 BBR v3 或统一使用同一算法
TSO/GSO 与 pacing 冲突 大段合并导致突发 配合 fq 使用,或调整 TSO 分段大小
容器网络 MTU 不一致 路径 MTU 发现失败,cwnd 计算错误 确保容器网络 MTU 一致
iperf3 测试结果不稳定 单线程限制、CPU 瓶颈 使用 -P 多线程,绑定 CPU 核心
Kubernetes 中 BBR 不生效 Pod 内无法设置 sysctl 使用 securityContext.sysctls 或在节点级别配置
窗口缩放因子(Window Scale)被禁用 最大窗口 64KB,高 BDP 链路完全无法工作 确保 tcp_window_scaling = 1

12.2 快速诊断命令

# 查看当前连接的拥塞控制状态
ss -tinp | head -20

# 详细的 TCP 连接信息(包括 cwnd、rtt、retrans)
ss -ti dst 10.0.0.1

# 输出示例:
# cubic wscale:7,7 rto:204 rtt:1.5/0.5 ato:40 mss:1460
# cwnd:10 ssthresh:7 send 77.9Mbps retrans:0/3 rcv_space:29200

# 实时监控拥塞窗口变化
watch -n 0.5 'ss -ti dst 10.0.0.1 | grep cwnd'

# 查看 TCP 统计信息
nstat -az TcpExt* | grep -E 'Retrans|ECN|Cong'

# 使用 tcptrace 分析抓包文件
tcpdump -i eth0 -w capture.pcap host 10.0.0.1
tcptrace -S capture.pcap

12.3 选择拥塞控制算法的决策树

你的场景是什么?
│
├── 数据中心内部(RTT < 1ms)
│   ├── 支持 ECN? ──是──> DCTCP
│   └── 不支持? ──────> CUBIC
│
├── 广域网(RTT > 10ms)
│   ├── 高 BDP(> 1MB)?
│   │   ├── 可以部署 BBR v3? ──是──> BBR v3
│   │   └── 不可以? ──────────────> CUBIC
│   └── 低 BDP? ──────────────────> CUBIC 或 Reno 均可
│
├── 移动网络 / WiFi
│   └── ──────────────────────────> BBR v3(避免 bufferbloat)
│
└── 卫星链路
    └── ──────────────────────────> BBR v3 + PEP(性能增强代理)

12.4 个人观点

关于 BBR 的炒作与现实:BBR 刚发布时被很多人视为银弹,但几年的实践表明它并非万能。BBR v1 的公平性问题在真实互联网上造成了不少麻烦——一条 BBR 流可以轻易饿死共享链路上的 CUBIC 流。好在 Google 团队并未停止迭代,BBR v3 在公平性上的改进令人满意。

关于 CUBIC 的生命力:不要低估 CUBIC。它已经在互联网上稳定运行了近二十年,经过了无数边界情况的锤炼。在大多数场景下,CUBIC “just works”。除非你有明确的性能问题并且测量数据表明切换算法能带来收益,否则保留 CUBIC 是最安全的选择。

关于 ECN/L4S 的未来:我对 L4S 持谨慎乐观态度。理论上它能实现微秒级排队延迟和零丢包,但现实中 ECN 的部署率仍然很低,中间设备的兼容性问题层出不穷。L4S 在数据中心内部会率先普及,但在公共互联网上的全面部署可能还需要十年以上。

关于 QUIC 的影响:QUIC 将拥塞控制从内核移到用户空间是一次架构性的进步。这意味着拥塞控制算法可以像应用一样快速迭代——Google 可以在一周内将新算法推送到全球数十亿 Chrome 用户。这种部署速度是 TCP 内核实现无法企及的。长远来看,QUIC 可能成为拥塞控制创新的主要载体。

关于工程师的选择:作为工程师,我们不应该盲目追新。在选择拥塞控制算法时,最重要的一步不是选算法,而是测量——用 iperf3、ss、tcptrace 等工具搞清楚你的网络到底是什么样的,瓶颈在哪里,然后用数据驱动决策。没有测量的优化就是猜测。


参考文献

  1. V. Jacobson, “Congestion Avoidance and Control,” ACM SIGCOMM, 1988.
  2. D. Chiu, R. Jain, “Analysis of the Increase and Decrease Algorithms for Congestion Avoidance in Computer Networks,” Computer Networks and ISDN Systems, 1989.
  3. S. Ha, I. Rhee, L. Xu, “CUBIC: A New TCP-Friendly High-Speed TCP Variant,” ACM SIGOPS Operating Systems Review, 2008.
  4. N. Cardwell et al., “BBR: Congestion-Based Congestion Control,” ACM Queue, 2016.
  5. N. Cardwell et al., “BBR v2: A Model-based Congestion Control,” IETF 104, 2019.
  6. J. Iyengar, I. Swett, “QUIC Loss Detection and Congestion Control,” RFC 9002, 2021.
  7. K. Nichols, V. Jacobson, “Controlling Queue Delay,” ACM Queue, 2012.
  8. M. Alizadeh et al., “Data Center TCP (DCTCP),” ACM SIGCOMM, 2010.
  9. B. Briscoe et al., “Low Latency, Low Loss, Scalable Throughput (L4S),” RFC 9330, 2023.
  10. L. Kleinrock, “Power and Deterministic Rules of Thumb for Probabilistic Problems in Computer Communications,” ICC, 1979.

上一篇: 学习索引 下一篇: 滑动窗口与流控

相关阅读: - 主动队列管理 - 限流算法


By .