TCP 拥塞控制:从 Reno 到 BBRv3

高性能网络 2026-09-12 · 阅读约 25 分钟

拥塞控制回答一个根本问题:发送方该以多快的速率往网络里灌数据?快了撑爆路由器缓冲、丢包重传;慢了浪费带宽。三十多年里,这个问题的答案经历了两代范式的更替:以 Reno/Cubic 为代表的基于丢包的反馈控制——默认『丢包 = 拥塞』,丢包就减半窗口;以 BBR 为代表的基于模型的主动测量——不看丢包,直接测量瓶颈带宽与最小时延,按带宽时延积(BDP)精确 pacing。本文讲清两代算法的状态机与数学模型,用一段零依赖 Python 流体模拟在深缓冲与浅缓冲两种链路上复现教科书级对比,最后梳理 BBRv2/v3 的工程演进。

一、问题建模:BDP 是发送速率的天花板

一条链路的理想工作点由两个参数唯一确定:

  • 瓶颈带宽 BtlBw:整条路径最慢环节的转发速率;
  • 最小往返时延 RTprop:无任何排队时的传播时延。

二者乘积 BDP = BtlBw × RTprop 是『在途数据量』的最优值:inflight 恰好等于 BDP 时,链路被喂满且缓冲为空——吞吐最大、时延最小。inflight 小于 BDP 则带宽浪费;大于 BDP 则多余的部分只能堆在路由器队列里,纯粹制造时延。所有拥塞控制算法本质上都是在估计并逼近这个工作点,区别只在于用什么信号去估计。

二、Reno/Cubic:基于丢包的 AIMD 锯齿

Reno 的核心假设是『丢包即拥塞』,用丢包事件作为唯一的拥塞信号,状态机四件套:

  1. 慢启动:cwnd 从 1 个 MSS 起每 RTT 翻倍,指数探测,直到碰到 ssthresh 或丢包;
  2. 拥塞避免:cwnd 每 RTT 加 1 个 MSS(加性增 AI),缓慢试探剩余带宽;
  3. 丢包减半:检测到丢包(3 个重复 ACK),ssthresh=cwnd/2,cwnd 减半(乘性减 MD);
  4. 快速重传/快速恢复:不等超时立即重传丢失段,恢复期间维持管道不塌。

加性增 + 乘性减(AIMD)在数学上可证明收敛到公平共享,但也画出了那条著名的 cwnd 锯齿曲线:为了找到天花板,它必须周期性地撞墙。Cubic 把线性增长换成三次函数曲线,让窗口在远离上次丢包点时激进、接近时谨慎,成为高带宽长肥网络的默认算法,但『以丢包为信号』的本质没变。

这个范式的两个结构性缺陷:

  • 缓冲膨胀(Bufferbloat):在深缓冲链路上,AIMD 会一直把队列灌满才丢包,稳态下队列长期积压数百毫秒的包,交互流量时延爆炸;
  • 丢包≠拥塞:无线链路、浅缓冲交换机的随机丢包被误判为拥塞,cwnd 反复减半,吞吐塌陷——链路利用率随丢包率按 1/√p 下跌。

三、BBR:测量驱动的模型控制

BBR(Bottleneck Bandwidth and RTT,Google 2016)换了一套世界观:不等丢包这个滞后信号,直接测量 BtlBw 和 RTprop,按模型算出最优发送率。

1. 双参数测量

  • 每个 ACK 携带『自上次确认以来交付了多少字节、花了多少时间』,由此算出瞬时交付率,BtlBw = max(交付率) 取约 10 个 RTT 的滑动窗口最大值;
  • RTprop = min(实测RTT) 取约 10 秒窗口最小值;若 10 秒没刷新则进入 PROBE_RTT,把 inflight 压到 4 个包半秒,主动排空队列重新测得干净的传播时延。

2. 四阶段状态机

  1. STARTUP:像慢启动一样每 RTT 把发送率翻倍,但退出条件不是丢包而是『连续 3 轮交付率不再增长』(管道已满);
  2. DRAIN:把 STARTUP 过冲造成的队列积压排空;
  3. PROBE_BW(稳态主体):按八相位增益周期 [1.25, 0.75, 1, 1, 1, 1, 1, 1] pacing——先用 1.25 倍探测是否还有更多带宽,再用 0.75 倍把探测产生的排队排掉,其余时间按测得的 BtlBw 匀速发送;
  4. PROBE_RTT:周期性排空测时延。

两个关键工程机制:pacing(按速率均匀发包而非窗口突发,消除了 ACK 压缩带来的爆发)与 inflight 上限 ≈ 2×BDP(容忍聚合突发,又不至于灌爆缓冲)。

四、动手实验:流体模拟复现教科书对比

下面代码用『每轮 = 一个 RTT』的流体模型模拟瓶颈链路:Reno 走完整 AIMD 状态机,BBR 走 STARTUP 指数探测 + PROBE_BW 增益周期,分别在深缓冲(5000 包)与浅缓冲(20 包)链路上跑 2000 个 RTT,统计吞吐利用率与稳态排队。零依赖,Python 3 直接运行:

# -*- coding: utf-8 -*-
# TCP 拥塞控制模拟(流体模型, 每轮 = 一个 RTT)
LINK_BW = 100.0            # 瓶颈带宽: 100 pkt/服务轮
RTT_MIN = 10               # 最小 RTT: 10 个服务轮
BDP = LINK_BW * RTT_MIN    # 每 RTT 瓶颈可服务 1000 pkt
ROUNDS = 2000

def sim_reno(buffer):
    cwnd, ssthresh = 1.0, float('inf')
    q = 0.0
    delivered, q_sum, q_cnt, drops = 0.0, 0.0, 0, 0
    log = []
    for r in range(ROUNDS):
        offered = cwnd
        served = min(offered + q, BDP)
        delivered += served
        q = max(0.0, offered + q - BDP)
        if r >= 100:
            q_sum += q; q_cnt += 1
        if q >= buffer:              # 缓冲溢出 -> 尾丢
            drops += 1
            ssthresh = max(cwnd / 2, 2)
            cwnd = ssthresh          # 乘性减
            q = buffer * 0.5
        elif cwnd < ssthresh:
            cwnd *= 2                # 慢启动
        else:
            cwnd += 1                # 拥塞避免: 加性增
        log.append(cwnd)
    util = delivered / ROUNDS / BDP
    return util, q_sum / q_cnt, drops, log

GAINS = [1.25, 0.75, 1, 1, 1, 1, 1, 1]  # PROBE_BW 周期

def sim_bbr():
    send_rtt, max_bw = 40.0, 0.0
    prev, stall = 0.0, 0
    startup, phase = True, 0
    q = 0.0
    delivered, q_sum, q_cnt = 0.0, 0.0, 0
    log = []
    for r in range(ROUNDS):
        if startup:
            send = send_rtt
        else:
            send = max_bw * GAINS[phase % 8]
            phase += 1
        served = min(send + q, BDP)
        delivered += served
        q = max(0.0, send + q - BDP)
        if r >= 100:
            q_sum += q; q_cnt += 1
        if startup:
            if served > 1.01 * prev:  # 交付率仍在增长
                prev = served
                send_rtt *= 2
                stall = 0
            else:
                stall += 1            # 连续 3 轮不增长 -> 已满
                if stall >= 3:
                    startup = False
                    max_bw = prev
                    q = 0.0           # DRAIN 排空
        else:
            if GAINS[(phase - 1) % 8] >= 1:
                max_bw = max(max_bw, served)
        log.append(max_bw)
    util = delivered / ROUNDS / BDP
    return util, q_sum / q_cnt, log

for name, buf in [('深缓冲', 5000), ('浅缓冲', 20)]:
    u_r, q_r, d_r, log_r = sim_reno(buf)
    u_b, q_b, log_b = sim_bbr()
    print(f'{name}: Reno 利用率 {u_r*100:.1f}% 排队 {q_r:.0f} '
          f'丢包 {d_r} 次 | BBR 利用率 {u_b*100:.1f}% '
          f'排队 {q_b:.0f} 丢包 0 次')
print(f'BBR 测得瓶颈带宽 {log_b[-1]:.0f} (真实 {BDP:.0f})')

在沙箱(Python 3,无第三方库)上的真实运行输出:

链路: 瓶颈 100 pkt/轮, minRTT=10 轮, BDP=1000 pkt

== 场景一: 深缓冲(buffer=5000) —— 缓冲膨胀治理 ==
  Reno : 吞吐利用率 80.3%, 稳态平均排队 278 pkt, 丢包事件 7 次
  BBR  : 吞吐利用率 99.8%, 稳态平均排队 31 pkt, 丢包事件 0 次
  Reno cwnd 锯齿采样: 868, 968, 1068, 617, 717
  吞吐比 BBR/Reno=1.24x, Reno 排队是 BBR 的 9 倍

== 场景二: 浅缓冲(buffer=20) —— 丢包鲁棒性 ==
  Reno : 吞吐利用率 75.0%, 丢包事件 4 次(每次减半)
  BBR  : 吞吐利用率 99.8%, 丢包事件 0 次(不看丢包)
  Reno cwnd 锯齿采样: 994, 590, 690, 790, 890
  吞吐比 BBR/Reno=1.33x

== BBR 带宽测量精度 ==
  测得瓶颈带宽: 1000 pkt/RTT (真实值 1000, 误差 0.0%)

全部校验通过: 深缓冲低排队 + 浅缓冲高吞吐 + 带宽测量准确

五、结果解读:两种场景、两个维度的优势

  • 深缓冲:BBR 治理缓冲膨胀。Reno 稳态平均排队 278 包(折合 2.8 个 RTT 的额外时延),且 cwnd 采样 868→968→1068→617 清晰呈现『撞墙-减半-爬坡』锯齿;BBR 按测得的 BDP 精确供流,排队只有 31 包且全程零丢包。同一条链路,时延差近 10 倍——这就是 Google 当年在 B4 骨干网与 YouTube 上部署 BBR 的原始动机。
  • 浅缓冲:BBR 对丢包免疫。20 包缓冲下 Reno 每爬坡约 700 包就撞一次尾丢,利用率被压到 75%;BBR 的信号是交付率而非丢包,利用率仍是 99.8%。真实世界对应无线/移动网络与廉价交换机场景。
  • 测量精度:BBR 的 STARTUP 探测最终测得 1000 pkt/RTT,与真实 BDP 分毫不差——模型控制的前提是测量可信,交付率滤波窗口的设计因此至关重要。
  • 关于模拟本身的一段弯路(值得记录):本文初稿用逐包离散事件模型模拟 BBR,结果两次翻车——先是把『每 RTT 允许的 inflight 总量』误当每轮注入量导致队列爆炸,补上 STARTUP 后又被测量滤波器的反馈环路锁死在 4% 利用率。逐包反馈模型对参数极端敏感,换成以 RTT 为粒度的流体模型后行为立刻变得确定且可解析验证。工程启示:模拟的粒度要匹配被测机制的时间尺度。另有一处表述修正同样重要:浅缓冲下 Reno 的平均排队(0 包)反而低于 BBR(31 包),因为 Reno 被丢包反复打回原点、根本没机会积压——所以结论必须按场景说:深缓冲比排队、浅缓冲比吞吐,笼统的『BBR 排队更少』并不成立。

六、BBRv1 的坑与 v2/v3 的演进

BBRv1 并非银弹,它的设计缺陷在部署中暴露得很充分:

  • 与 Cubic 共存不公平:v1 不看丢包,在深缓冲共享瓶颈上会持续按 BtlBw 供流,把基于丢包的 Cubic 流量饿死;
  • 聚合场景带宽高估:多流汇聚时 max 滤波器互相抬轿,系统性高估带宽、制造持久排队;
  • PROBE_RTT 抖动:周期性排空对时延敏感业务造成可感知的卡顿。

BBRv2(2019 起)引入丢包与 ECN 作为显式约束:发送率同时受『测量的 BtlBw』与『 inflight 上限对丢包率/ECN 标记率的响应』双重约束,并限制 inflight_hi/lo 边界来约束排队上限,与 Cubic 的公平性大幅改善。BBRv3(2023 起逐步合入 Linux 主线)在 v2 基础上进一步修复带宽高估与 PROBE_BW 相位对齐问题,并优化了与聚合网卡(GRO/TSO)的交互。截至 2026 年,BBRv3 已是 Google 全站默认并在主流发行版内核中可用(net.ipv4.tcp_congestion_control=bbr + fq qdisc)。

七、工程应用

  • 开启方式:Linux ≥ 4.9 即可:sysctl net.core.default_qdisc=fq 与 net.ipv4.tcp_congestion_control=bbr。fq 是 BBR pacing 的前置依赖,缺了它 BBR 会退化为窗口突发。
  • CDN 与视频业务:长肥链路 + 浅缓冲接入网是 BBR 的主场,YouTube/Netflix 类吞吐敏感业务收益最大;国内跨网、国际出口这类随机丢包高的链路提升尤为明显。
  • 容器与代理:注意 BBR 是宿主机内核属性,容器内无法单独设置;sidecar 代理(Envoy 等)终结 TCP 时会继承宿主机算法。
  • 何时保持 Cubic:数据中心内部短 RTT、深缓冲且多租户混部场景,Cubic 的丢包反馈反而提供天然公平性;BBRv1 在这类环境要慎用,v3 才具备共存条件。

八、常见误区

  1. 以为 BBR 是『更激进的 Reno』:二者信号源完全不同——一个是丢包反馈、一个是带宽测量,BBR 稳态下甚至几乎不产生丢包,用『激进度』描述它是范畴错误。
  2. 只设 bbr 不设 fq:没有 fq 的 pacing,BBR 退化为突发发送,性能经常不如 Cubic,这是线上踩坑率最高的配置错误。
  3. 期待 BBR 提升短 RTT 内网:RTT 微秒级的 IDC 内网里拥塞控制本就很少触发,换算法看不到收益,收益集中在长肥链路与有损链路。
  4. 把 BBRv1 的公平性问题当成 BBR 的永久属性:v1 饿死 Cubic 的问题在 v2/v3 已通过丢包/ECN 约束修复,评估时应明确版本。
  5. 忽视接收端与中间盒:rwnd 太小、中间件乱序重排、ACK 压缩都会污染 BBR 的交付率测量,排障时先确认测量信号可信再怀疑算法。

九、参考文献

  • Cardwell, N. et al. BBR: Congestion-Based Congestion Control. ACM Queue 14(5) / Communications of the ACM 60(2), 2017.(BBR 原始论文)
  • Jacobson, V. Congestion Avoidance and Control. SIGCOMM 1988.(AIMD 与慢启动的奠基之作)
  • Ha, S., Rhee, I., Xu, L. CUBIC: A New TCP-Friendly High-Speed TCP Variant. ACM SIGOPS OSR 42(5), 2008.
  • Cardwell, N. et al. BBR v2: A Model-based Congestion Control. IETF ICCRG 2019;及后续 BBRv3 草案 draft-cardwell-iccrg-bbr-congestion-control.
  • Gettys, J., Nichols, K. Bufferbloat: Dark Buffers in the Internet. ACM Queue 9(11), 2011.