TCP 既要可靠又要高效,而网络的瓶颈链路容量是动态变化的:发送太快,中间节点排队甚至丢弃;发送太慢,带宽被白白浪费。拥塞控制就是 TCP 用来"探测可用带宽、避免压垮网络、又不浪费带宽"的自适应机制。本指南深入慢启动、拥塞避免、快速重传恢复等经典算法,以及 CUBIC、BBR 两大现代实现,最后给出真实可用的调优方案。
关键概念:拥塞控制解决"网络容量未知且动态变化“的问题。核心三件套:ssthresh(慢启动阈值)、拥塞窗口 cwnd、接收窗口 rwnd。实际发送量 = min(cwnd, rwnd) × MSS,谁小听谁的。
一、核心概念
1.1 窗口:谁是瓶颈
发送窗口约束:
实际发送量 ≤ min(拥塞窗口 cwnd, 接收窗口 rwnd)
└ 网络容量(探测)┘ └ 接收方能力(对端通告)┘
关键指标:
带宽延迟积 BDP = 带宽 × RTT (链路"在途"最大数据量)
例:100Mbps × 50ms → 0.625 MB ≈ 40 个 MSS
若 cwnd < BDP → 带宽永远打不满(经典"高带宽长链路"问题)
1.2 拥塞窗口 vs 接收窗口
- rwnd(接收窗口):接收方告诉发送方"我还能接收多少”,是接收方的流控。
- cwnd(拥塞窗口):发送方估计"网络当前能承受多少",是发送方的拥塞控制。
- 两者独立,取小者作为实际发送上限。接收方缓冲够大但网络拥塞,瓶颈在 cwnd;网络通畅但对端读得慢,瓶颈在 rwnd。
ℹ️ 排障直觉:带宽打不满,依次看 cwnd(网络)、rwnd(接收方缓冲)、应用读取速度(对端消费太慢导致窗口坍缩)。
二、经典算法
2.1 慢启动(Slow Start)
慢启动规则:
初始 cwnd = 初始窗口(Linux 默认 10 个 MSS)
每收到一个 ACK,cwnd 增加一个 MSS → cwnd 每个 RTT 翻倍
效果:指数增长,快速逼近链路可用带宽
直到 cwnd ≥ ssthresh → 进入拥塞避免
为什么叫"慢"启动:
相比早期协议一上来就塞满窗口,慢启动是从小窗口"试探"
实际增速反而很快(指数),命名源于历史上下文
丢失检测:超过 RTO 未收到 ACK → 认为网络拥塞
// 概念演示:慢启动 cwnd 指数增长(每 RTT 翻倍)
cwnd := 10.0
for cwnd < ssthresh {
cwnd *= 2 // 10→20→40→80...
if cwnd >= ssthresh {
break
}
}
// 进入拥塞避免后:每 RTT 只加 1 个 MSS(线性)
2.2 拥塞避免(AIMD)
进入拥塞避免后:
每 RTT 增加 1 个 MSS(线性增长,慢而稳)
一旦检测到丢包 → cwnd 减半(乘性减小)
丢包是"拥塞信号",用减半回应而非归零,保持吞吐
AIMD(加增乘减):
加法增大:cwnd += 1 MSS / RTT
乘法减小:遇丢包 cwnd = cwnd × 0.5(ssthresh 同步为一半)
特点:逼近网络容量时两侧震荡,收敛于可用带宽附近
2.3 快速重传与快速恢复
快速重传(Fast Retransmit):
收到 3 个重复 ACK(dup ACK)→ 直接重传丢包,不必等 RTO 超时
原理:重复 ACK 说明后面的段已到、中间的段丢了
快速恢复(Fast Recovery):
超时重传后 → cwnd = 1(跌回慢启动)← 最坏情况
重复 ACK 触发时 → ssthresh = cwnd/2,cwnd = ssthresh + 3
恢复过程中尽量不跌回慢启动,吞吐损失更小
算法演进:
Reno:经典 AIMD,一次恢复
New Reno:多包丢失时避免窗口来回跌
SACK:精确告知对端哪些段丢了,恢复更快
三、CUBIC:Linux 默认算法
3.1 设计思路
CUBIC 是 Linux 内核 2.6.19+ 起的默认拥塞控制算法,主要针对高带宽、高延迟的"长肥网络"(LFN)优化。
CUBIC 核心理念:
增长基于"距上次丢失事件的时间 t",而非 RTT 次数的线性
即使用 RTT 波动大的链路,也能稳定地占领带宽附近
在高 BDP 链路上表现远优于 Reno
窗口曲线(三次函数):
W(t) = C × (t - K)³ + Wmax
其中 K = ∛(Wmax × β / C)
Wmax:上次发生拥塞时的窗口
β = 0.3 (乘性减小因子)
C = 0.4 (增长曲率,与按 BDP 相关)
行为特征:
离开上次拥塞时快速增长(凹区间)
接近上次窗口高峰 Wmax 附近增长放缓(凸区间)
对 RTT 波动不敏感,多流共享时更公平
3.2 对比
| 维度 | Reno | CUBIC |
|---|---|---|
| 增长依据 | RTT 次数 | 距上次丢包时间 |
| 高 RTT 链路 | 增长慢 | 增长不受 RTT 拖累 |
| 高 BDP | 差 | 优(长肥网络首选) |
| 波动 | 较大 | 更平滑 |
| 默认 | 早期内核 | Linux 2.6.19+ / RHEL7 默认 |
ℹ️ 核心:CUBIC 把"增长速率"从"每 RTT 一步"改成了"按时间推进的立方曲线",让高延迟链路的窗口恢复更快、吞吐更高,同时保持多流之间的公平性。
四、BBR:基于延迟的拥塞控制
4.1 哲学转变
传统算法(CUBIC/Reno,丢包感知):
以「丢包」为拥塞信号 → 窗口太大丢包就减半
问题:在缓冲很深的网络(bufferbloat)里,
cwnd 被排队占满后才丢包,延迟高、抖动大
("丢包才发现拥塞",为时已晚)
BBR(Bottleneck Bandwidth and Round-trip time):
以「实测带宽 + 最小 RTT」为信号,不主动追丢包
发送速率贴近瓶颈带宽,不人为在队列里堆数据
→ 吞吐高、延迟低、排队少
发送速率 ≈ 瓶颈带宽(BBR 的核心)
PROBE_BW 阶段周期探测带宽变化,维持高位
PROBE_RTT 阶段收缩到最小 RTT,避免排队误判
4.2 BBR 状态机
BBR 四个状态:
1. STARTUP:指数增发探测瓶颈带宽(类似慢启动)
2. DRAIN:发现瓶颈后降低发送速率,排空多余队列
3. PROBE_BW:周期性探测带宽变化,保持高位吞吐
4. PROBE_RTT:周期性用最小 RTT 校准,防止排队误导
落地启用:
sysctl net.ipv4.tcp_congestion_control=bbr
内核需 ≥ 4.9
常见发行版已支持(内核 4.9+ 均内置)
# 查看当前拥塞控制算法与可用算法
sysctl net.ipv4.tcp_congestion_control
cat /proc/sys/net/ipv4/tcp_available_congestion_control
⚠️ 注意:BBR 并非万能。它在"有丢包的拥塞网络"里表现极好,但在"本身低丢包且需严格排队公平"的部分生产链路,可能相对激进。切换前务必在灰度链路上做带宽/RTT/丢包率 A/B 对比。
五、TCP 内核调优
5.1 Linux 常用参数
# /etc/sysctl.conf 常用调优项
# ---- 缓冲区与窗口 ----
net.ipv4.tcp_rmem = 4096 65536 33554432 # 读缓冲 min default max
net.ipv4.tcp_wmem = 4096 65536 33554432 # 写缓冲
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_sack = 1 # 选择性确认,改善乱序恢复
# ---- 拥塞控制与特性 ----
net.ipv4.tcp_congestion_control = bbr # 或 cubic
net.ipv4.tcp_fastopen = 3 # TFO:TLS/HTTP 首个往返携带数据
net.ipv4.tcp_mtu_probing = 1 # MTU 黑洞自动降级探测
net.ipv4.tcp_ecn = 1 # 显式拥塞通知(可选,需对端支持)
# ---- 队列与连接 ----
net.core.somaxconn = 4096 # 监听队列上限(配合应用 backlog)
net.ipv4.tcp_max_syn_backlog = 4096 # SYN 队列
sysctl -p
5.2 判断链路是否被窗口限制
排查步骤:
1. 用 iperf3 测瓶颈带宽 B
2. 用 ping 测 RTT → BDP = B × RTT
3. ss -tin 查看当前 cwnd 与发送速率
4. 若 cwnd 长期远小于 BDP → 窗口/缓冲不够,调大 rmem/wmem
5. 若速率贴近 B 但延迟高 → 是排队/拥塞,优先切 BBR 或降缓存
常见误区:
- 盲目调大所有缓冲 → 内存浪费 + 延迟上升
- 只看带宽不看 RTT → 高 BDP 链路的"假满"
- 只顾内核不顾应用 → 应用读得慢,窗口照样坍缩
六、常见避坑
| 坑 | 现象 | 对策 |
|---|---|---|
| cwnd 太小 | 高 BDP 链路始终打不满带宽 | 调大 rmem/wmem,考虑 BBR |
| 内核版本旧 | 无 BBR,长肥链路低吞吐 | 升级内核 ≥ 4.9,启用 BBR |
| bufferbloat 深排队 | 延迟高、抖动大 | 切 BBR 减少排队,或降缓冲 |
| 盲目调大缓冲 | 内存浪费、延迟上升 | 按 BDP 保守设置 |
| 只看带宽不看 RTT | 误判链路已满 | 用 BDP = 带宽 × RTT 判断 |
| 应用消费慢 | 窗口坍缩、吞吐低 | 优化应用读取/处理,调 rwnd |
| 多流共享不公平 | 单流霸占带宽 | 用 CUBIC 保证公平,BBR 注意灰度 |
| TFO/NAT 兼容性 | 握手加速在部分路径失效 | 灰度验证,兼容性优先 |
七、最佳实践清单
□ 高 BDP 链路优先启用 BBR(内核 4.9+),先做 A/B 对比
□ 确认 rmem/wmem 上限 ≥ BDP(带宽 × RTT 的在途字节)
□ 长连接应用配 keepalive 与合理缓冲,不让窗口复位
□ 排查"打不满带宽"时先算 BDP,再动参数
□ 用 ss -tin、iperf3 做真实测量,不靠猜
□ 压测看带宽 + P99 延迟双维度,别只看吞吐
□ TFO/ECN 等新特性灰度验证后推广
□ 网络优化先从"减少丢包与排队"入手,再调参数
一句话原则
TCP 用窗口探测网络,丢包感知(CUBIC)看拥塞减半,
BBR 看带宽与延迟贴近瓶颈;先测 BDP 再调参数。
小结
拥塞控制是 TCP 在"容量未知、动态变化"的网络中自我调节的生存机制。慢启动用指数探测快速逼近可用带宽,AIMD 拥塞避免在瓶颈附近线性增长、丢包减半收敛,CUBIC 用基于时间的立方曲线让高延迟链路吞吐更高、更平滑,而 BBR 直接以"实测带宽 + 最小 RTT"为信号,不追丢包、不堆队列,兼顾高吞吐与低延迟。落地记住五件事:先算 BDP 再调参、高延迟链路启用 BBR 并灰度验证、缓冲按 BDP 保守设置、用 ss/iperf 实测不靠猜、压测看带宽与延迟双维度。当你既能用窗口模型解释"为什么打不满带宽",又能用 BBR 把长链路吞吐再拉一截时,TCP 传输优化就从"调参数"变成了"看本质"。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。