TCP 拥塞控制演进:从 Reno 到 BBR
前面几篇文章聚焦于 P2P 网络的架构与实现,但 P2P 应用的性能瓶颈往往不在协议层本身,而在于底层的传输协议——TCP 拥塞控制。一个 BitTorrent 节点可能同时维持数百个 TCP 连接,每个连接都在独立地进行拥塞控制。如果拥塞控制算法选择不当,P2P 节点的带宽利用率会大幅下降,尤其是在高延迟或存在随机丢包的链路上。理解拥塞控制的演进,不仅能帮助 P2P 开发者优化传输性能,也是深入理解互联网传输层运作的必经之路。
TCP 拥塞控制是互联网稳定运行的基石。自 1988 年 Van Jacobson 在 SIGCOMM 上发表经典论文以来,拥塞控制算法经历了近四十年的演进,从基于丢包的启发式方法发展到基于网络模型测量的精确控制。
拥塞控制的基本原理
所有 TCP 拥塞控制算法都围绕拥塞窗口(CWND,Congestion Window)进行工作——它表示发送端在未收到确认前可以发送的最大数据量:
| |
这个公式揭示了 TCP 吞吐量的两个约束:发送端窗口(CWND)和接收端窗口(Receiver Window),两者中的较小者决定了实际吞吐量。CWND 由拥塞控制算法动态调整,而接收窗口由接收端的缓冲区大小决定。
不同算法的核心差异体现在三个维度:
- CWND 增长速度(慢启动 vs 拥塞避免)——决定了连接启动和带宽探测的效率
- 拥塞检测信号(丢包 vs 延迟 vs 带宽测量)——决定了算法对网络状态的感知方式
- 拥塞后的降速策略(乘法减小 vs 模型驱动的调整)——决定了拥塞事件后的恢复速度
TCP Reno(1988)
Van Jacobson 提出的经典 AIMD(Additive Increase Multiplicative Decrease)模型是拥塞控制的奠基之作。它的核心思想可以用一句话概括:轻负载时线性增长,检测到拥塞时指数退避。
flowchart TD
subgraph 慢启动
SS["CWND = IW (≈10 segs)<br/>每收到 ACK: CWND += 1 MSS<br/>指数增长"]
end
subgraph 拥塞避免
CA["CWND ≥ ssthresh<br/>每 RTT: CWND += 1 MSS<br/>线性增长"]
end
subgraph 丢包处理
LOSS["检测到 3 个重复 ACK"]
FR["快恢复<br/>ssthresh = CWND/2<br/>CWND = ssthresh"]
TO["超时<br/>ssthresh = CWND/2<br/>CWND = 1 MSS<br/>重新慢启动"]
end
SS -->|"CWND ≥ ssthresh"| CA
CA -->|"丢包"| LOSS
LOSS --> FR
LOSS --> TO
FR -->|"新 ACK"| CA
TO --> SSReno 的慢启动阶段以指数方式快速探测可用带宽——每收到一个 ACK,CWND 增加一个 MSS(最大报文段长度),相当于每个 RTT 翻倍。当 CWND 达到慢启动阈值(ssthresh)后,进入拥塞避免阶段,每个 RTT 只增加 1 个 MSS,形成线性增长。一旦检测到丢包(三个重复 ACK),立即将 ssthresh 设为当前 CWND 的一半,CWND 也降到 ssthresh,进入快恢复阶段。
Reno 的局限:在高带宽高延迟链路(如跨洋光纤)上表现保守。假设一条 100ms RTT、1Gbps 带宽的链路,BDP 约为 12.5MB。如果发生丢包,CWND 减半后需要数千个 RTT 才能恢复到原窗口大小——这意味着数分钟的恢复时间,严重浪费带宽。
TCP CUBIC(2005)
CUBIC 是当前 Linux 内核的默认拥塞控制算法,由 Injong Rhee 等人提出,专门为解决 Reno 在高 BDP(带宽延迟积)网络中的低效问题而设计。
立方增长函数
CUBIC 最核心的创新是用一个三次函数来驱动窗口增长:
$$W(t) = C \times (t - K)^3 + W_{max}$$
其中 $t$ 是距离上次拥塞事件的时间,$W_{max}$ 是拥塞发生时的窗口大小,$C$ 是一个缩放常数(默认 0.4),$K = \sqrt[3]{W_{max} \times \beta / C}$ 是窗口恢复到 $W_{max}$ 所需的时间。
flowchart TD
A["上次拥塞事件后<br/>经过时间 t"] --> B["CUBIC 窗口函数"]
B --> C["W(t) = C × (t - K)³ + Wmax"]
C --> D{"窗口位置"}
D -->|"Wmax 以下<br/>凹增长"| E["快速恢复<br/>追赶原窗口"]
D -->|"Wmax 附近<br/>缓慢探测"| F["小心搜索<br/>避免再次拥塞"]
D -->|"Wmax 以上<br/>凸增长"| G["积极探测<br/>新带宽上限"]
E --> H{丢包}
F --> H
G --> H
H -->|"是"| I["CWND × 0.7<br/>记录新 Wmax"]
I --> ACUBIC 的三大特性
CUBIC 的核心理念是用时间而非ACK 到达来驱动窗口增长,这意味着窗口增长速率与 RTT 无关:
- 凹增长:远低于 $W_{max}$ 时快速恢复。丢包发生后,CUBIC 的窗口函数急剧上升,迅速将 CWND 拉回到拥塞前的水平——这正是 Reno 表现最差的区域。
- 凸增长:超过 $W_{max}$ 后积极探测新高点。当 CWND 超过上次拥塞的窗口值后,函数曲线开始加速,主动寻找新的带宽上限。
- RTT 公平性:不同 RTT 的流在固定时间内达到相同的窗口增量。在 Reno 中,短 RTT 的流拥塞窗口增长更快,容易"饿死"长 RTT 流。CUBIC 消除了这种 RTT 偏差。
为什么 CUBIC 取代了 BIC
CUBIC 的前身是 BIC(Binary Increase Congestion Control),后者使用二分搜索来探测带宽边界——这种方法在高带宽网络中表现优秀,但在低带宽网络中过于激进,且窗口函数的形状不平滑。CUBIC 用一个平滑的三次函数替代了 BIC 的分段二分搜索,既保留了高 BDP 下的效率,又改善了低带宽网络中的友好性。
BBR 的范式革命(2016)
Google 在 2016 年发布的 BBR(Bottleneck Bandwidth and Round-trip propagation time)彻底改变了拥塞控制的传统范式。核心洞察是:丢包不等同于拥塞。
在无线网络、蜂窝网络等场景中,数据包可能因为信号干扰、信道衰减等原因丢失,但这并不意味着网络链路发生了拥塞。传统基于丢包的算法在这种情况下会错误地降低发送速率,造成严重的带宽浪费。
flowchart LR
subgraph 传统丢包基础算法
L["Reno/CUBIC<br/>观察信号:丢包"]
L1["假设:丢包 = 拥塞"]
L2["行为:丢包 → 降速"]
L --> L1 --> L2
end
subgraph BBR 模型基础算法
M["BBR<br/>观察信号:带宽+RTT"]
M1["模型:测量 BtlBw + RTprop"]
M2["行为:在 BDP 点运行"]
M --> M1 --> M2
end
L2 -.->|"在丢包链路中<br/>性能大幅下降"| CRI["对比"]
M2 -.->|"在丢包链路中<br/>维持高吞吐"| CRIBBR 的关键创新
- 不依赖丢包信号:在丢包率高达 15% 的链路上仍能维持高吞吐,而 CUBIC 在 1% 丢包率时吞吐量就会大幅下降。
- 主动测量带宽延迟积(BDP):通过测量瓶颈带宽(BtlBw)和传播延迟(RTprop),计算出链路的最佳承载数据量,在 BDP 点运行,既不产生队列也不造成浪费。
- 避免 Bufferbloat:即使缓冲区很大,BBR 也不会填满缓冲区,从而避免大量排队延迟——这对延迟敏感的应用(如实时音视频、在线游戏)至关重要。
Linux 实操:查看与切换算法
了解了算法原理后,我们来看看如何在 Linux 系统中实际操作。以下命令可以查看和切换当前系统的拥塞控制算法:
| |
输出示例:
| |
如果输出中没有 bbr,说明内核没有加载 BBR 模块。可以手动加载:
| |
需要注意的是,拥塞控制算法的更换是系统全局的——所有新建的 TCP 连接都会使用新算法。已经建立的连接会继续使用创建时的算法,不受切换影响。
性能对比
不同拥塞控制算法在有损链路下的表现差异巨大。以下是在相同网络条件下(100ms RTT,100Mbps 带宽)的吞吐量对比数据:
| 丢包率 | Reno | CUBIC | BBR |
|---|---|---|---|
| 0% | 100% | 100% | 100% |
| 0.1% | ~50% | ~85% | ~99% |
| 1% | ~10% | ~30% | ~95% |
| 3% | ~3% | ~12% | ~88% |
| 5% | ~1% | ~5% | ~80% |
| 10% | ~0.5% | ~2% | ~60% |
从上表可以清晰看出:
- 在 0.1% 低丢包率下,CUBIC 还能保持约 85% 的吞吐量,Reno 已经损失了一半带宽。
- 在 1% 丢包率下,CUBIC 跌至约 30%,而 BBR 仍能保持 95% 以上——这正是无线网络、蜂窝网络常见的丢包范围。
- 在 5% 丢包率下,Reno 和 CUBIC 基本不可用,而 BBR 仍能维持约 80% 的吞吐量。
这也解释了为什么 P2P 应用在无线网络环境中传输性能不佳的问题根源——不仅仅是 P2P 协议的问题,底层的 TCP 拥塞控制算法同样关键。
BBR 版本演进
BBR 自发布以来经历了三个主要版本的迭代:
| 版本 | 时间 | 核心改进 |
|---|---|---|
| BBR v1 | 2016 | 首次提出基于模型的拥塞控制,实现高吞吐低延迟;但存在与 Reno/CUBIC 共存公平性差、STARTUP 阶段丢包率高的问题 |
| BBR v2 | 2019 | 引入 ECN 信号支持,改善与 Reno/CUBIC 共存时的公平性,降低 STARTUP 阶段的丢包率 |
| BBR v3 | 2023 | 修复带宽收敛 bug,STARTUP 增益从 2.89 降至 2.77,进一步降低排队延迟,优化短流性能 |
版本演进的关键驱动力
BBR 的版本演进反映了学术界和工业界对"理想拥塞控制"理解的深化:
- 共存公平性:BBR v1 在与其他算法共享瓶颈链路时过于激进,会抢占 CUBIC 流的带宽。v2 和 v3 通过引入更保守的竞争策略逐步改善了这个问题。
- STARTUP 阶段的震荡:初始版本中 STARTUP 阶段的指数增益过高(2.89),导致频繁的过度探测和丢包。v3 将增益降到 2.77,显著减少了启动震荡。
- 带宽收敛:在多流共享瓶颈的场景中,v1 和 v2 存在带宽分配不稳定的问题。v3 通过算法优化实现了更快、更稳定的收敛。
下一篇文章将深入 BBR 算法的内部机制和实现细节,包括其四状态机(STARTUP/DRAIN/PROBE_BW/PROBE_RTT)的完整工作流程。
参考资料
- Jacobson, V. (1988). Congestion avoidance and control. SIGCOMM ‘88.
- Ha, S., Rhee, I., & Xu, L. (2008). CUBIC: a new TCP-friendly high-speed TCP variant. ACM SIGOPS.
- Cardwell, N., et al. (2016). BBR: Congestion-based congestion control. ACM Queue.
- Cardwell, N., et al. (2022). BBR Congestion Control (IETF Draft). https://datatracker.ietf.org/doc/draft-cardwell-iccrg-bbr-congestion-control/
- Xu, L., Harfoush, K., & Rhee, I. (2004). Binary increase congestion control (BIC) for fast long-distance networks. INFOCOM 2004.