跳转至

21. 立方的野心:Cubic 与高 BDP 网络的攻防

上一章用具体数字暴露了 Reno 的结构性瓶颈:在 1 Gbps / 100ms 的链路上,一次丢包后恢复满窗口需要七分钟;换成 10 Gbps / 200ms 的跨洲专线,这个数字变成近五个小时。问题的根源不是实现不好,而是"每 RTT 加 1 MSS"的线性增长在管道容量达到几千上万个 MSS 时,从结构上就注定了恢复速度与管道容量成反比。

本章讲 Cubic 如何回应这个挑战。它不换信号源——丢包仍然是唯一的拥塞判据——而是在 Reno 的基本框架上做了一次精准手术:用一条三次函数曲线替换线性增长,用 HyStart 抑制慢启动过冲,用 TCP-Friendly Region 保证在低 BDP 网络上不欺负 Reno。六节依次覆盖:线性恢复在更极端场景下的困境、BIC 到 Cubic 的演进因果、三次函数曲线的设计直觉、HyStart 的检测逻辑、TCP-Friendly Region 的平衡术,以及 Cubic 作为丢包驱动算法的根本天花板。


21.1 长肥管道的数学困境:线性恢复为什么撑不住

上一章末尾已经用具体数字暴露了 Reno 的恢复瓶颈:1 Gbps / 100ms 链路上丢一次包,线性恢复需要 428 秒(七分钟);而 Mathis 公式更进一步证明,要在这条链路上跑满带宽,丢包率不能超过 2 × 10⁻¹⁰——任何真实网络都无法满足。

这些数字已经够触目惊心了。但 Cubic 的设计者面对的场景还要更极端。把管道再拉长一个数量级:

场景                      带宽       RTT      BDP(MSS)  Reno 恢复时间
───────────────────────────────────────────────────────────────────────
1G 跨国链路(中美)       1 Gbps     150ms    ~10274      ~12.8 分钟
10G 跨洲专线              10 Gbps    200ms    ~171233     ~4.75 小时

近五个小时。一条花了大价钱租来的 10G 专线上,一次丢包就意味着整个下午都在"爬坡"。

问题的根源不是 Reno 的实现不好,而是"每 RTT 加 1 MSS"这条增长规则的结构性限制——恢复速度与管道容量无关,管道越大,恢复时间越长,两者成正比。这个困境在 2000 年代初随着链路带宽的快速增长变得不可忽视。研究界开始意识到:必须找到一种比线性更快、但又不失稳定性的增长方式。这就是 Cubic 的出发点——它不是在"改进 Reno",而是在回应一个时代级别的工程难题。


21.2 BIC 到 Cubic:从二分搜索到连续函数

在 Cubic 之前,同一组研究者(Xu、Rhee 等人,2004)先设计了 BIC(Binary Increase Congestion Control)。BIC 的核心思想很有启发性:把窗口增长当成对"最大安全窗口"的二分搜索。

BIC 的逻辑是这样的。丢包发生时,当前 cwnd 就是"不安全"的上界(记为 W_max),减半后的值是已知的"安全"下界。接下来的增长不再匀速线性,而是在这两个边界之间做二分搜索——每一步取中点值,快速逼近 W_max。

丢包前:cwnd = 1000 MSS = W_max
减半后:cwnd = 500 MSS

BIC 搜索过程:
第 1 步:目标 = (500 + 1000) / 2 = 750   跳到 750
第 2 步:目标 = (750 + 1000) / 2 = 875   跳到 875
第 3 步:目标 = (875 + 1000) / 2 = 937   跳到 937
...

对比 Reno 的线性增长——从 500 到 1000 需要 500 个 RTT——BIC 只需要约 log₂(500) ≈ 9 步就能逼近。这是指数级的加速。

BIC 在高 BDP 网络上的恢复速度确实远超 Reno。但它有几个实践中暴露出来的问题:

第一,增长曲线不连续。BIC 把增长分成若干段——二分搜索阶段、线性探索阶段——段与段之间有跳变。不连续的窗口变化会在网络中引起突发流量,对路由器队列不友好。

第二,对 Reno 的公平性不好。BIC 在低 BDP 网络上的增长比 Reno 激进,当 BIC 流和 Reno 流共享同一条链路时,Reno 会被挤压。在 2000 年代初互联网上大量 Reno 流仍在运行的背景下,这个问题很严重。

第三,行为不够可预测。BIC 的分段逻辑在不同网络条件下可能走入不同的分支,难以用一个统一的数学模型来分析和调参。

Cubic(Ha、Rhee、Xu,2008)的设计突破在于:用一条连续的三次函数来拟合 BIC 的分段行为。三次函数天然具有 BIC 那种"先快后慢再快"的形状——在远离 W_max 时增长快(追赶阶段),接近 W_max 时增长慢(谨慎试探),超过 W_max 后又加速(探索新容量)。而且它是一条平滑的连续曲线,没有分段跳变,行为可预测,易于分析。

从离散搜索到连续函数,是从"工程凑合能用"到"数学上优雅且工程上可靠"的跃升。BIC 验证了"非线性恢复"的方向是正确的,Cubic 则给出了一个足够干净的数学形式来承载这个方向。


21.3 三次函数增长曲线的设计直觉

Cubic 的核心是一条三次函数增长曲线。RFC 9438 §4.2 给出了窗口增长公式:

W(t) = C × (t - K)³ + W_max

四个参数各有角色:

  • W_max:上次丢包前的窗口大小——Cubic 对管道容量的"记忆"
  • K:从减半窗口恢复到 W_max 所需的时间,计算公式为 K = ∛(W_max × (1 - β) / C)
  • C:控制增长激进度的常数,RFC 9438 推荐 0.4
  • t:距本次拥塞避免阶段开始的时间(秒)

这条曲线为什么是"先快后慢再快"?看它在三个阶段的行为:

cwnd
  │                                        ╱
  │                                      ╱   ← 凸区域:超过 W_max 后
  │                                    ╱       加速探索新容量
  │  W_max ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ •─ ─ ─ ─ ─
  │                              ╱  │
  │                           ╱     │  ← 平台区:接近 W_max 时
  │                        ╱        │    增速放缓,谨慎试探
  │                    ╱
  │                ╱                    ← 凹区域:丢包后快速
  │           ╱                          恢复到 W_max 附近
  │      ╱
  │  •╱  ← 丢包后减至 β × W_max
  │╱
  └──────────────────────────────────────→ 时间 t
       0          K                    t

凹区域(t < K):窗口远低于 W_max,三次函数的斜率很大,增长很快。这是追赶阶段——发送方知道管道至少能装 W_max 那么多数据(上一轮到了那个水位才丢包),所以大步快跑去恢复。

平台区(t ≈ K):窗口接近 W_max,三次函数的斜率趋近于零,增长几乎停滞。这是谨慎试探阶段——上次就是在 W_max 附近丢的包,这次别急,慢慢看看是不是真的到了极限。

凸区域(t > K):窗口超过 W_max,三次函数再次加速。这是探索阶段——也许管道容量已经变大了(其他流退出了、路由变了),值得激进地试探新的上限。

这种"追赶 → 谨慎 → 探索"的三段式行为,精确对应了丢包恢复的合理策略。对比 Reno 的匀速线性增长——不管离 W_max 远还是近,每 RTT 都只加 1 MSS——Cubic 把力量分配得更聪明。

还有两个设计决策值得注意。

β 的选择:0.7 而不是 Reno 的 0.5。 Reno 丢包后窗口减半,Cubic 丢包后只减到 70%。RFC 9438 §4.6 解释了原因:较高的 β 意味着丢包后保留更多窗口,在高 BDP 网络中保持更高的链路利用率。代价是收敛速度比 Reno 慢——回到公平状态需要更多轮次。但在现代互联网高度多路复用的环境下,丢包很少完全同步发生,这个代价是可以接受的。

增长以时间为自变量,而非以 RTT 为单位。 Reno 的"每 RTT 加 1 MSS"意味着 RTT 短的流增长快、RTT 长的流增长慢——两条 Reno 流共享链路时,短 RTT 的流会抢走更多带宽。Cubic 的窗口增长公式 W(t) 以绝对时间 t 为自变量,不依赖 RTT。这意味着在不同 RTT 的链路上,同样的带宽和同样的丢包模式下,Cubic 流的增长行为更一致。RFC 9438 §4.8 指出,两个不同 RTT 的 Cubic 流的吞吐量比率与 RTT 比率成线性关系,而 Reno 是二次方关系——Cubic 对长 RTT 流更公平。

用同一个场景来对比恢复速度。1 Gbps / 100ms 链路,W_max = 8562 MSS,丢包后:

Reno:cwnd 减半至 4281 → 线性恢复 4281 个 RTT → 428 秒
Cubic:cwnd 减至 0.7 × 8562 = 5993 → 三次函数快速逼近 8562
       K = ∛(8562 × 0.3 / 0.4) ≈ 18.5 秒
       约 20 秒内恢复到 W_max 附近

从七分钟到二十秒。这就是三次函数对线性增长的结构性优势。


21.4 HyStart:慢启动阶段的过冲抑制

Cubic 不只改了拥塞避免阶段的增长曲线。它还在慢启动阶段做了一个重要的补丁——HyStart(Hybrid Slow Start)。

慢启动的过冲风险是它的固有缺陷:指数增长在最后一步可能让 cwnd 从管道容量的一半直接翻倍到两倍。多出来的一半数据涌入路由器队列,导致大面积丢包和严重的排队时延。在高带宽链路上,这个过冲尤其严重——如果管道容量是 5000 MSS,最后一步从 2500 翻到 5000,2500 个多余的包同时灌入队列。

Reno 的慢启动只有一个退出条件:等到丢包发生。这等于是先撞墙再刹车——撞墙本身就造成了伤害。HyStart 的思路是在撞墙之前就察觉到"快到墙了"。

HyStart 用两种方式检测排队信号:

RTT 增长检测。 在慢启动过程中持续收集 RTT 样本,跟踪全局最小 RTT 和每一轮次的最小 RTT。当积累了足够多的样本后(通常需要至少 8 个),如果当前轮次的最小 RTT 显著高于全局最小 RTT——比如高出 4ms 以上——说明路由器队列已经开始积压,管道即将被灌满。此时提前退出慢启动,进入拥塞避免。

ACK 间距检测。 在管道未拥塞时,ACK 到达的间距通常很紧凑。当路由器开始排队时,包的延迟分散增大,ACK 到达的间距也会拉开。如果相邻两个 ACK 的到达间隔超过阈值(如 2ms),HyStart 认为网络可能已经开始拥塞,也会触发提前退出。

RTT 样本
  │                    ★ ← 当前轮次 min RTT(已上升)
  │             ───────────── HyStart 阈值(min_rtt + 4ms)
  │  ─ ─ ─ ─ ─ ─ ─ ── ─ ─  全局 min RTT(本底时延)
  └──────────────────────────→ 时间
       正常        ← 排队开始 →
                   HyStart 检测到
                   信号,提前退出
                   慢启动

HyStart 退出后做什么?它把当前 cwnd 设为 ssthresh,然后切换到拥塞避免阶段——Cubic 的三次函数增长接管。相比"等到丢包再反应",这种提前退出温和得多:没有大面积丢包,没有窗口骤降,只是从指数增长平滑过渡到三次函数增长。

HyStart 有一个固有的灵敏度权衡。阈值太敏感——比如 RTT 只涨了 1ms 就退出——会导致过早退出慢启动,大量带宽来不及利用。阈值太迟钝——要涨 10ms 才退出——又回到了 Reno 那种"快撞墙才反应"的问题。这个阈值的选择没有完美答案,只有在具体网络环境下的合理折中。RFC 9438 推荐使用 HyStart++ (RFC 9406) 作为慢启动改进方案,它在传统 HyStart 的基础上进一步优化了退出判定逻辑。

HyStart 不是一个独立的算法,而是 Cubic 整体方案的一部分。如果说三次函数曲线解决了"拥塞避免阶段恢复太慢"的问题,HyStart 解决的是"慢启动阶段刹不住车"的问题。两者配合,Cubic 才在慢启动和拥塞避免两个阶段都比 Reno 表现更好。


21.5 Cubic 与 Reno 的公平性:TCP-Friendly Region 的平衡术

Cubic 在高 BDP 网络上远超 Reno 的恢复速度,引出了一个关键问题:如果 Cubic 流和 Reno 流共享一条链路,Reno 会不会被挤死?

答案取决于网络的 BDP。在高 BDP 网络上,Cubic 理应比 Reno 更快——这正是它被设计出来的目的。但在低 BDP 网络上——比如数据中心内部或同城短链路——Reno 本身已经工作得很好,Cubic 不应该抢走更多带宽。

RFC 9438 §4.3 定义了 TCP-Friendly Region 来解决这个问题。Cubic 在收到每个 ACK 时,同时计算两个窗口值:

W_cubic(t) = C × (t - K)³ + W_max       ← Cubic 三次函数给出的窗口
W_est(t)   = W_est + α × acked / cwnd   ← 模拟 Reno AIMD 给出的窗口

其中 α_cubic = 3 × (1 - β_cubic) / (1 + β_cubic)。代入 β_cubic = 0.7,得到 α_cubic ≈ 0.53。

然后取两者中较大的那个作为最终 cwnd。

这个"取较大值"的设计看似简单,背后的逻辑却很精巧:

在低 BDP 网络上,W_max 本身就不大,三次函数曲线的增长幅度很小。此时 W_est(Reno 模拟值)反而更大。Cubic 会跟随 W_est 增长——它的行为退化为 Reno。这意味着在低 BDP 网络上,Cubic 流和 Reno 流获得的带宽几乎相同——不会欺负 Reno。

在高 BDP 网络上,W_max 很大,三次函数曲线的追赶速度远超 Reno 的线性增长。此时 W_cubic 大于 W_est,Cubic 按自己的三次函数增长——显著快于 Reno。

cwnd
  │                      ╱ W_cubic(三次函数)
  │                    ╱
  │  W_max ─ ─ ─ ─ ─╱─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
  │               ╱╱
  │            ╱╱        ╱ W_est(Reno 模拟)
  │         ╱╱        ╱
  │      ╱╱        ╱
  │   ╱╱       ╱
  │ ╱╱     ╱
  │╱╱  ╱
  │╱╱  ← 交叉点之前,W_est > W_cubic(Reno 友好区域)
  └──────────────────────────────────────→ 时间
     ↑ 这里 Cubic 跟随 Reno

这个设计体现了"向后兼容"的工程智慧。互联网上的拥塞控制算法不是一夜之间全部切换的——Cubic 部署的初期,绝大多数流还在跑 Reno。如果 Cubic 在所有场景下都比 Reno 激进,Reno 流的带宽会被大量侵占,导致生态失衡。TCP-Friendly Region 保证了 Cubic 只在 Reno 确实力不从心的高 BDP 场景下才超越它,在 Reno 本身表现足够好的低 BDP 场景下保持平等。

这也是 Cubic 能在 2006 年成为 Linux 默认拥塞控制算法的关键原因之一。它不只是"更快"——如果只是更快,在低 BDP 场景下的公平性问题会让它无法被广泛部署。它是"在该快的地方快,在该公平的地方公平"。对于一个要成为操作系统默认选择的算法来说,这种"不破坏已有生态"的保证比纯粹的性能优势更重要。


21.6 丢包派的巅峰与天花板

Cubic 是丢包驱动拥塞控制的巅峰之作。它用三次函数曲线精确解决了高 BDP 网络下的恢复困境——从 Reno 的七分钟压缩到二十秒。HyStart 在慢启动阶段提前检测排队信号,避免了指数增长的过冲灾难。TCP-Friendly Region 保证了与 Reno 的公平共存,让它能安全地成为默认算法。自 2006 年进入 Linux 内核至今,Cubic 统治了互联网的拥塞控制生态近二十年。RFC 9438(2023)将它正式标准化,同时更新了 RFC 5681 的拥塞控制框架。

但 Cubic 没有跳出"丢包 = 拥塞"这个根本假设。这个假设在两种场景下失效,恰好对应两种截然不同的"错误信号"问题。

第一种是无线网络中的随机丢包。无线链路上,信号衰减、干扰、切换都会导致丢包,这些丢包和网络拥塞无关。Cubic 无法区分"路由器队列满了"和"无线信号差了"——它会把所有丢包都当作拥塞信号,触发窗口减少。在一个 5% 随机丢包率的无线链路上,Cubic 会反复减速,带宽利用率远低于链路实际能承载的水平。

第二种是深缓冲区链路上的 bufferbloat。现代路由器的队列缓冲区越来越大。Cubic(和 Reno 一样)只有在丢包时才知道减速——这意味着它会先把缓冲区填满,然后才开始减窗口。缓冲区越深,填满之前积累的排队时延越高。一条 RTT 本应 20ms 的链路,可能因为缓冲区被塞满而飙到 200ms。用户感受到的不是丢包,而是反应迟钝——网页加载变慢、视频通话卡顿。丢包驱动的算法对这种"没丢包但已经很拥塞"的情况完全无感。

第三个结构性限制更根本:Cubic 永远无法知道"管道到底有多大"。它只能在丢包和不丢包之间反复试探——增长到丢包,减下来,再增长,再丢包。管道的真实容量(带宽 × 时延)始终是一个黑盒。W_max 不是管道容量,它只是"上次恰好到达的水位"——可能正好等于管道容量,也可能远低于(因为其他流占用了带宽)或远高于(因为缓冲区提供了额外的容量假象)。

这些限制不是 Cubic 的实现 bug,而是"丢包驱动"范式的结构性天花板。在这个范式内,Cubic 已经做到了极致——三次函数、HyStart、TCP-Friendly Region,每一步都是在不改变信号源的前提下最聪明的优化。

如果不再依赖丢包信号,而是直接建立网络管道的带宽和时延模型——直接估计管道有多大,而不是等到灌爆了才知道——那将是一条完全不同的道路。