22. 模型的革命:BBR 三代演进与算法共存
Cubic 把丢包驱动推到了巅峰——三次函数曲线在高 BDP 网络上的恢复速度是 Reno 的二十倍,HyStart 抑制了慢启动过冲,TCP-Friendly Region 保证了与 Reno 的公平共存。但上一章的结尾也暴露了这个范式的三个结构性天花板:随机丢包被误判为拥塞,深缓冲区掩盖了真实排队,发送方永远无法知道管道到底有多大。
本章讲一条完全不同的道路。2016 年,Cardwell、Cheng、Jacobson 等人在 Google 提出了 BBR(Bottleneck Bandwidth and Round-trip propagation time),第一次认真地说:不要等到管道溢出才知道它有多大,直接去测量它。六节依次覆盖:BBR 的核心模型与丢包驱动的根本区别、v1 在真实网络中暴露的 inflight 膨胀问题、v2 引入 inflight 上限和丢包轮次追踪的公平性修正、v3 用 loss-based bound 和 ProbeBW 子状态实现的收敛改进、三种算法混跑时的竞争动态,以及为什么协议库不能只押注一种答案。
22.1 BBR 的核心模型:BtlBw × RTprop = BDP
Reno 和 Cubic 共享同一个世界观:发送方对网络管道一无所知,只能不停地加速,等到丢包了才知道"满了"。BBR 问了一个不同的问题:能不能不等管道溢出,直接测量它有多大?
答案的基础是两个物理量。
BtlBw(Bottleneck Bandwidth,瓶颈带宽)。一条网络路径由多段链路串联而成,每段链路有各自的容量。整条路径能承载的最大传输速率,由容量最小的那段决定——这就是瓶颈带宽。BBR 通过持续测量"已送达的数据量 / 对应的时间间隔"来估计这个值,并用一个滑动窗口最大值过滤器记住近期观测到的最大带宽。
RTprop(Round-trip Propagation Time,往返传播时延)。这是数据包在路径上跑一个来回的最短时间——不含任何排队时延,只有信号在光纤或铜线中的物理传播时间。BBR 通过跟踪观测到的最小 RTT 来逼近这个值。
两个量的乘积给出管道的容量:
BDP 就是网络管道在任意时刻"刚好能装下"的数据量——所有数据都在传播途中,没有一个字节在排队。
BBR 的控制策略直接建立在这个模型上:
这和 Reno/Cubic 的策略形成了鲜明对比。来看两种方法在同一个场景下的行为:
场景:1 Gbps / 50ms 链路,BDP ≈ 6.25 MB
Reno/Cubic 的做法:
不知道管道多大 → 持续增加 cwnd → 填满管道 → 填满缓冲区
→ 丢包 → 减速 → 再增长 → 再丢包 → 循环往复
结果:cwnd 在 BDP 到 BDP + 缓冲区 之间锯齿振荡
BBR 的做法:
测量 BtlBw = 1 Gbps,RTprop = 50ms
→ 设置发送速率 = 1 Gbps,inflight 目标 = 6.25 MB
→ 管道刚好填满,缓冲区几乎为空
结果:持续高吞吐,排队时延接近零
这就是为什么 BBR 在理论上能同时实现高吞吐和低时延——它不需要填满缓冲区就能知道管道有多大。Reno/Cubic 把缓冲区当作"探测管道容量的代价",BBR 把缓冲区当作"应该避免占用的资源"。
但这个模型有一个根本性的测量困境:BtlBw 和 RTprop 不能同时被精确测量。
要测量真实的 BtlBw,需要让管道满载——发送足够多的数据来把瓶颈链路跑满。但管道满载时,数据会在瓶颈链路前排队,RTT 上升,此时观测到的 RTT 不再是 RTprop,而是 RTprop + 排队时延。
要测量真实的 RTprop,需要让管道接近空载——减少 inflight 数据量,直到路径上没有排队。但管道空载时,实际传输速率远低于 BtlBw,此时无法准确估计瓶颈带宽。
← 测 RTprop 更准 → ← 测 BtlBw 更准 →
inflight: 很低 ≈ BDP >> BDP
RTT: ≈ RTprop RTprop + 少量排队 RTprop + 大量排队
实际速率: 低于 BtlBw ≈ BtlBw ≈ BtlBw(但在排队)
这个不可同时性是 BBR 所有复杂性的根源。BBR 的四种运行模式——Startup、Drain、ProbeBW、ProbeRTT——本质上都是在"测带宽"和"测时延"之间交替切换,试图用时间分割的方式逼近两个无法同时获得的测量值。而 v1 到 v3 的三代演进,很大程度上都在回答一个问题:这种交替测量在真实网络中遇到了什么障碍,又该如何修正。
22.2 v1 的突破与代价:吞吐优异,但 inflight 膨胀挤压 Cubic
BBR v1(Cardwell 等人,2016)的四种运行模式构成了一个完整的探测-稳态循环:
Startup:连接建立后进入指数增长阶段,pacing_gain 设为 2/ln(2) ≈ 2.885。这个增益让发送速率每轮翻倍,在 O(log₂(BDP)) 个 RTT 内找到 BtlBw。退出条件不是丢包,而是带宽估计连续三轮不再增长——说明瓶颈链路已经被跑满,继续加速只会堆积队列。
Drain:Startup 阶段的指数增长会在管道中堆积约 2 个 BDP 的队列。Drain 模式把 pacing_gain 降到 Startup 的倒数(约 0.35),快速排空队列,直到 inflight 降到 BDP 水平。
ProbeBW:稳态运行模式,BBR 大约 98% 的时间在这里度过。v1 使用一个 8 相增益循环:
第一相(5/4):pacing_gain 为 1.25,以略高于 BtlBw 的速率发送。如果可用带宽增加了(比如其他流退出了),这个探测会发现新的 BtlBw。如果带宽没变,多出的 25% 数据会进入缓冲区,产生少量排队。
第二相(3/4):pacing_gain 为 0.75,排空上一相产生的队列。
后六相(1):以 BtlBw 匀速发送,不产生也不消耗队列。
ProbeRTT:每 10 秒触发一次。v1 把 cwnd 降到 4 个 MSS,持续 200ms,清空路径上的队列来刷新 RTprop 估计。
v1 在 Google 的生产环境中展示了惊人的效果。在 B4 广域网上,BBR 的吞吐量比 Cubic 高出 2 到 25 倍。在 YouTube 全球边缘网络上,中位数 RTT 降低了 53%,发展中国家超过 80%。在 5% 随机丢包率下,BBR 仍能维持接近满带宽的吞吐,而 Cubic 在超过 0.1% 丢包率时就开始严重下降。
但 v1 在更广泛的真实网络中很快暴露了严重问题。
BtlBw 估计偏高导致 inflight 膨胀。 v1 用滑动窗口最大值过滤器估计 BtlBw——它记住过去若干轮次中观测到的最高带宽。问题在于:当多个流共享同一条链路时,某个流可能在其他流暂时退让(比如进入 ProbeRTT)时观测到一个偏高的带宽值。这个偏高的估计被最大值过滤器"锁定",在后续多轮中持续使用。结果是:v1 发送的数据量超过了自己应得的公平份额,多余的数据堆在路由器缓冲区中——这就是 inflight 膨胀。
inflight 膨胀挤压 Cubic 流。 v1 膨胀的 inflight 填满了缓冲区。Cubic 流看到缓冲区被占满后开始丢包,被迫反复减速。Hock 等人(2017)的实验测量表明,在深缓冲区场景下,单个 BBR v1 流可以占据 70% 以上的带宽,Cubic 流只能分到剩余部分。这不是暂时的不公平——只要 v1 的 BtlBw 估计持续偏高,这个挤压就会持续存在。
缓冲区使用量
│
│ ███████████████████████ ← BBR v1 inflight 膨胀
│ ███████████████████████ 填满了大部分缓冲区
│ ██████ ← Cubic 的空间被压缩
│ ██████ 被迫频繁丢包和减速
│─────────────────────────── 缓冲区上限
│
└──────────────────────→ 时间
v1 的核心矛盾。 BBR 的模型假设网络是"一条带宽固定的管道"——每个流可以独立测量 BtlBw 和 RTprop。但真实网络里有共享缓冲区、有竞争流、有动态变化的可用带宽。当模型的假设和现实不匹配时,最大值过滤器会系统性地高估带宽,导致行为偏离模型预期。
v1 的问题不是"模型驱动"这个方向错了,而是第一个版本对真实网络的复杂性估计不足。它优美的理论模型在"干净"的环境(单流、固定带宽)中表现完美,但在多流竞争、缓冲区共享的互联网中暴露了严重的公平性缺陷。
22.3 v2 的公平性修正:引入 loss tolerance 与 inflight 上限
BBR v2(Cardwell、Cheng 等人,2019)针对 v1 暴露的公平性问题做了一次关键的设计转向:不再完全无视丢包,开始给 inflight 设置显式的上限。
v2 的核心改变是引入了两个新的状态变量:
inflight_hi——inflight 的安全上界。当丢包发生时,v2 记录当时的 inflight 水位,并将其乘以 0.7 作为后续发送的上限参考。这意味着 v2 不再允许 inflight 无限膨胀——即使 BtlBw 估计偏高,inflight_hi 也会在丢包时把实际发送量拉回来。
inflight_lo——inflight 的保守下界。当连续多轮出现丢包时,inflight_lo 进一步降低,提供一个更保守的发送水位。
v1 的 cwnd 目标: cwnd = cwnd_gain × BDP
(只看模型,不管丢包)
v2 的 cwnd 目标: cwnd = min(cwnd_gain × BDP, inflight_hi)
(模型和丢包历史,取较小值)
这个 min 操作是 v2 最关键的一步。它意味着 v2 的发送量受到双重约束:模型给出理论上界,inflight_hi 给出经验安全边界。无论哪个更小,都以更小的那个为准。
v2 还引入了丢包轮次追踪(loss_event_count_in_round_)——v1 完全不记录丢包历史,v2 开始跟踪"最近几轮有没有丢包"。这个信息被用来调整 inflight_hi 的更新策略:
丢包时:inflight_hi = max(inflight_lo, inflight_hi × 0.7)
→ 每次丢包把上限削减 30%
无丢包时:inflight_hi += 1 MSS
→ 每轮缓慢恢复上限
这种"丢包时快速降、无丢包时缓慢升"的策略,和 AIMD 的精神异曲同工——只是作用对象从 cwnd 变成了 inflight_hi。v2 在"纯模型驱动"和"部分丢包感知"之间找到了一个折中点:它仍然用 BtlBw × RTprop 作为核心控制信号,但用丢包作为"模型可能出错"的安全阀。
v2 还开始响应 ECN(Explicit Congestion Notification)信号:当 ACK 携带 ECN-CE 标记时,inflight_hi 削减 5%。ECN 比丢包更早反映拥塞——路由器在队列快满但还没丢包时就可以标记 ECN。v2 对 ECN 的响应比对丢包更温和(5% vs 30%),体现了"信号越早,反应越轻"的设计哲学。
v2 的实际效果:inflight 膨胀明显减少,与 Cubic 共存时的公平性有了显著改善。在 v1 占据 70%+ 带宽的场景中,v2 把这个比例拉回到更合理的水平。
但 v2 仍然有局限。
第一,inflight_hi 的 0.7 衰减系数是经验性的选择。在某些极端场景下——比如缓冲区非常浅或丢包率波动剧烈——这个固定系数可能过于激进或过于保守。
第二,ProbeBW 仍然使用 v1 的 8 相增益循环。这个循环的探测-排空行为是固定模式,缺乏对当前网络状态的精细感知——不管网络是否需要探测,都按固定周期执行。
第三,ProbeRTT 仍然把 cwnd 降到 4 个 MSS——这在高带宽链路上意味着吞吐量骤降 99%+,持续 200ms。这个周期性的深度减速对延迟敏感的应用(如实时视频)影响很大。
v2 的演进方向已经清晰:BBR 从"完全忽略丢包"回退到"有条件地关注丢包"。这不是倒退——这是模型和现实磨合的必然结果。v2 解决了 inflight 膨胀的核心问题,但 ProbeBW 的固定增益循环和 ProbeRTT 的过度激进仍是待修的短板。
22.4 v3 的收敛:loss-based bound 与 Probe-RTT 改进
BBR v3 是截至目前最成熟的版本。它延续了 v2"模型为主、丢包为辅"的方向,但在两个关键维度上做了深度改进:ProbeBW 的状态机精细化,以及丢包/ECN 响应的量化升级。
ProbeBW 从固定增益循环进化为四个明确的子状态。
v1/v2 的 ProbeBW 是一个机械的 8 相增益循环——不管网络状况如何,都按固定模式轮转探测和排空。v3 把这个固定模式拆解为四个有语义的子状态,每个子状态都有自己的进入条件和退出判据:
ProbeBW 子状态(v3):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ DOWN │────→│ CRUISE │────→│ REFILL │────→│ UP │
│ gain=0.75│ │ gain=1.0 │ │ gain=1.0 │ │ gain=1.25│
│ 排空队列 │ │ 稳态巡航 │ │ 填充管道 │ │ 探测带宽 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
↑ │
└───────────────────────────────────────────────────┘
DOWN(pacing_gain = 0.75):主动降低 inflight,释放路径上的排队。退出条件是 inflight 降到 BDP 以下,且至少持续 1 个 RTT。这个状态确保在下一次探测前,路径上没有残留的队列——干净的起点才能得到准确的测量。
CRUISE(pacing_gain = 1.0):稳态发送,不主动探测也不排空。持续约 3 个 RTT。这个状态的存在是为了给其他流(尤其是 Cubic 流)留出调整窗口——如果 BBR 每个周期都在激进探测,Cubic 流没有机会恢复公平份额。
REFILL(pacing_gain = 1.0):在探测前重新填充管道到 BDP 水平。持续 1 个 RTT。为什么需要这个状态?因为 DOWN 阶段把 inflight 压到了 BDP 以下,如果直接从低水位进入 UP 探测,探测的起点太低,容易低估带宽。REFILL 先把管道"灌满",再启动探测。
UP(pacing_gain = 1.25):以 1.25 倍 BtlBw 发送,探测带宽是否增加。持续 1 个 RTT。如果观测到更高的带宽,更新 BtlBw;如果没有,多余的数据进入缓冲区,随后的 DOWN 阶段会排空。
这四个子状态比 v1/v2 的固定循环更有针对性。DOWN 确保干净起点,CRUISE 留出公平性空间,REFILL 保证探测精度,UP 执行实际探测。每个子状态的持续时间由网络状态决定(比如 DOWN 在 inflight 真正降到 BDP 以下时才退出),而不是像 v1 那样按固定的 RTT 轮数机械轮转。
loss-based bound:丢包重新进入决策,但方式与 Reno/Cubic 完全不同。
v3 引入了显式的丢包率阈值——默认 2%(loss_thresh_ = 0.02)。每一轮结束时,v3 计算本轮的丢包率:
如果 loss_rate > 2%,v3 认为"模型可能高估了可用带宽",将 inflight_hi 乘以 beta_loss(0.9):
这个设计与 Reno/Cubic 的丢包响应有本质区别。Reno 把每次丢包都当作"管道满了"的证据,直接砍半 cwnd。v3 做的是统计判断——只有当一轮中的丢包率超过阈值时才反应,个别的随机丢包不会触发减速。而且减速幅度(10%)远小于 Reno 的 50% 或 Cubic 的 30%。丢包在 v3 中不是"审判",而是"校准信号"——它告诉 BBR"你的模型可能有点偏,微调一下"。
ECN 响应更加积极。 v3 的 ECN 减速因子 beta_ecn = 0.85,比丢包更激进(15% vs 10%)。这看似反直觉——ECN 不是比丢包更早、更温和的信号吗?逻辑在于:ECN 标记意味着路由器队列已经接近满载但还没丢包,如果此时不果断减速,下一步就是丢包。v3 选择对 ECN 更积极地响应,以避免发展到丢包阶段。
ProbeRTT 改进。 v1 的 ProbeRTT 把 cwnd 降到 4 个 MSS,在高带宽链路上这意味着吞吐量几乎归零。v3 的 ProbeRTT 更加温和——虽然仍然每 10 秒触发、持续 200ms,但降低的幅度小于 v1,减少了周期性吞吐骤降对应用的冲击。
v3 的设计哲学可以总结为一句话:仍然以模型为主,但承认模型的不完美,用丢包率和 ECN 作为安全阀和校准工具。 如果说 v1 是"纯理想主义",v2 是"开始向现实妥协",v3 就是"在理想和现实之间找到了一个可工作的平衡点"。BBR 经过三代演进,已经从"完全忽视丢包"收敛到"以模型为主、以统计丢包率和 ECN 为辅"的混合策略。
22.5 算法共存的真实世界:BBR / Cubic / Reno 混跑时谁赢谁输
真实的互联网不是单一算法的实验室。同一条链路上,BBR 流、Cubic 流、Reno 流可能同时存在。它们用完全不同的信号来决定发送速率——BBR 看带宽和时延模型,Cubic 和 Reno 看丢包。当这些"说不同语言"的算法在同一条管道里竞争时,会发生什么?
这个问题的答案取决于至少三个维度:缓冲区深度、RTT 差异、流数量和生命周期。
缓冲区深度是最关键的变量。
在深缓冲区场景下(缓冲区 >> BDP),Cubic/Reno 会把缓冲区填满——它们只有在丢包时才减速,而深缓冲区要很久才丢包。BBR 的行为取决于版本:
深缓冲区(缓冲区 ≈ 5 × BDP):
BBR v1 vs Cubic:
v1 的 BtlBw 估计偏高 → inflight 膨胀 → 填满缓冲区
→ Cubic 被迫看到更多丢包 → 减速
结果:v1 占 70%+ 带宽,Cubic 被挤压
BBR v2/v3 vs Cubic:
inflight_hi 限制了膨胀 → inflight 接近 BDP
→ 但 Cubic 仍然填满缓冲区 → BBR 的 RTprop 估计被排队时延污染
结果:更公平,但 Cubic 仍可能略占优势
在浅缓冲区场景下(缓冲区 < BDP),局面反转。Cubic/Reno 频繁触发丢包,cwnd 反复被砍半,无法充分利用带宽。BBR 由于不依赖丢包信号,受到的影响小得多。
浅缓冲区(缓冲区 ≈ 0.5 × BDP):
BBR v3 vs Cubic:
Cubic 频繁丢包 → 反复减窗 → 吞吐量波动大
BBR 丢包率 < 2% 时不减速 → 吞吐量更稳定
结果:BBR 占优,Cubic 表现不佳
RTT 差异放大了不公平。
Reno 的公平性对 RTT 高度敏感——RTT 短的流增长快(每 RTT 加 1 MSS),RTT 长的流增长慢。两条 Reno 流的吞吐量比约为 RTT 比的平方的倒数——RTT 差两倍,带宽差四倍。
Cubic 对 RTT 更公平——它的增长以绝对时间而非 RTT 为自变量——但仍然不完全独立于 RTT。
BBR 的带宽估计理论上与 RTT 无关——BtlBw 是对传输速率的直接测量。但实际上,RTT 影响了 BBR 探测的频率和精度。长 RTT 的 BBR 流探测周期更长,对带宽变化的响应更慢。当短 RTT 和长 RTT 的 BBR 流竞争时,短 RTT 的流在探测-调整循环中有速度优势。
流数量和生命周期增加了不确定性。
实验室测试通常用几条长寿命流来评估公平性。但真实互联网上,大量短流(网页请求、API 调用)和少数长流(视频流、大文件下载)混合存在。短流在慢启动阶段就结束了,永远不进入稳态——它们的行为由慢启动策略决定,和稳态公平性几乎无关。长流之间的竞争才是公平性的主战场,但长流的数量和进出时间不断变化,使得稳态分析只是一种近似。
运营商的 AQM(主动队列管理)改变了游戏规则。
部分运营商在路由器上部署了 AQM 策略(如 CoDel、PIE),在队列还没满时就主动丢包或标记 ECN。AQM 把"丢包信号"从"缓冲区满了"变成了"队列开始积压了"——这让 Cubic/Reno 的反应更及时,也让 BBR 的 ECN 响应更有用。在有 AQM 的网络中,所有算法的行为都更接近"理想模型",公平性也更好。
综合来看,算法共存不是"谁更先进"的单维问题,而是一个多维度的系统性挑战:
场景 BBR(v3) Cubic Reno
────────────────────────────────────────────────────────
深缓冲区 较公平 占优 尚可
浅缓冲区 占优 波动大 波动大
高随机丢包 稳定 性能下降 性能急剧下降
有 AQM 良好 良好 良好
短 RTT 竞争 略占优 尚可 明显吃亏
长 RTT 竞争 更公平 较公平 不公平
没有一种算法在所有场景下都"赢"。这不是因为算法不够好,而是因为不同的控制哲学在不同的网络条件下各有优劣。这也是为什么 BBR 三代演进的核心驱动力始终是同一个问题:模型驱动的算法如何在一个丢包驱动占主导的互联网里存活并公平竞争。
22.6 协议库为什么不能只押注一种答案
三章走下来——Reno 建立了丢包驱动的基线,Cubic 在高 BDP 场景下将其推向极致,BBR 三代则在模型驱动与现实妥协之间反复校准——可以得出一个经过充分论证的结论:没有一种拥塞控制算法在所有场景下都是最优的。
这不是客套话。Reno 在数据中心内部(BDP 极小、丢包率极低)仍然够用,简单可预测。Cubic 在高 BDP 有线链路上表现卓越,统治了互联网近二十年。BBR 在浅缓冲区和随机丢包场景下有结构性优势,但在与 Cubic 的公平性上仍在进化。
对于一个面向真实世界的协议库来说,这意味着三件事。
第一,必须支持多种算法共存。不同的连接面对不同的网络环境——数据中心内部的短连接和跨洲际的视频流,不应该使用同一种算法。连接级别的算法选择不是"锦上添花",而是工程必需。
第二,必须为新算法的接入保留扩展空间。BBR 从 v1 到 v3 的演进表明,拥塞控制算法的优化远没有结束。一个只能硬编码三种算法的协议库,在 v4、v5 或全新算法出现时就需要大改架构。统一的拥塞控制接口和工厂模式不是过度设计,而是对"算法会持续进化"这个工程现实的直接回应。
第三,必须有验证手段来确认算法在不同场景下的行为。仅仅"实现了 BBR v3"不够——必须能证明它在深缓冲区不会挤压 Cubic,在浅缓冲区不会被 Cubic 抢走带宽,在高随机丢包下不会频繁减速。没有仿真和基准测试的拥塞控制实现,是不可信的。
BBR 三代的演进故事本身就是这个结论最好的证据。v1 的理论模型优美到令人赞叹,但它在真实网络中制造了严重的公平性问题。v2 开始向现实妥协,引入了丢包感知。v3 进一步收敛,把丢包和 ECN 重新纳入决策框架——但不是回到 Reno 的"丢包即审判",而是作为"模型校准信号"。这条演进路径说明:拥塞控制不存在一次性设计成功的银弹,只有持续的假设-验证-修正循环。
一个面向真实世界的协议库,必须同时容纳这些互相矛盾的控制哲学——不是因为选不出最好的,而是因为"最好的"取决于这条连接此刻面对的网络条件,而网络条件在连接建立之前无法预知。