Linux TCP 拥塞控制算法深度实战:BBR vs CUBIC 的生产部署与调优
当你的服务 P99 延迟被网络吃掉,当带宽利用率长期不足 60%,是时候深入 TCP 拥塞控制的内核世界了。本文不讲故事,只讲你怎么判断、怎么选、怎么调、怎么监控。
1. 拥塞控制本质:一个分布式 TCP 连接池的经济学问题
TCP 拥塞控制的哲学很简单:网络是共享资源,每一条连接都像一个自私的参与者。如果所有连接都全力发包,网络拥塞崩溃(congestion collapse),所有人的吞吐都归零。所以拥塞控制就是在"尽量多拿带宽"和"不挤垮网络"之间博弈。
Linux 内核定义了一个 struct tcp_congestion_ops,约 10+ 个回调函数构成算法的"虚函数表":
// include/net/tcp.h
struct tcp_congestion_ops {
struct list_head list;
u32 key;
u32 flags;
void (*init)(struct sock *sk); // 连接建立时初始化
void (*release)(struct sock *sk); // 连接释放
u32 (*ssthresh)(struct sock *sk); // 慢启动阈值
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked); // 拥塞避免阶段
void (*set_state)(struct sock *sk, u8 new_state); // 状态机切换
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
void (*in_ack_event)(struct sock *sk, u32 flags);
u32 (*undo_cwnd)(struct sock *sk); // F-RTO 撤销窗口
u32 (*sndbuf_expand)(struct sock *sk); // 发送缓冲区扩展
void (*cong_control)(struct sock *sk, const struct rate_sample *rs);
// BBR 用这个代替传统事件
get_info_t *get_info;
};
注意 cong_control 这个回调——这是 BBR 独有的。传统算法(CUBIC、Reno)用的是事件驱动的模型(cong_avoid 等),而 BBR 每 ACK 都调用 cong_control 来直接控制 pacing rate,这是两种架构思想的根本区别。
2. CUBIC:基于丢包的老兵
CUBIC 自 Linux 2.6.19(2006 年)起默认使用,至今仍是多数发行版的默认算法。
核心公式
CUBIC 的窗口增长函数不是线性的,而是三次函数:
Cwnd(t) = C × (t - K)³ + W_max
其中 K = ∛(W_max × (1 - β) / C)
W_max:上次拥塞事件时的窗口大小C:缩放因子(内核默认 0.4)β:乘性减少因子(默认 0.2,即丢包后窗口减为 80%)
曲线形状是"陡峭上升—平台探顶—缓慢爬升"的三阶段。
CUBIC 的三个工作模式
| 模式 | 公式区域 | 行为 |
|---|---|---|
| 凹区间 | t < K | 快速恢复到 W_max,类似 Reno 慢启动后的避让 |
| 凸区间 | t ≈ K | 稳住窗口探测最大带宽,不急于冒进 |
| 增长期 | t > K | 缓慢增大窗口直到再次丢包,实现高带宽利用 |
CUBIC 在缓冲区膨胀(Bufferbloat)下的致命伤
现代网络设备为了"不丢包"配置了大缓冲队列。CUBIC 的"丢包=拥塞"信号来得太晚——队列填满后经过 RTT 才触发丢包——这段时间内连接已经在持续注入更多包,结果是:
- 延迟飙升(bufferbloat 可达数百毫秒)
- 丢包率 5%~15% 但吞吐不稳
- 对短流(RTT 短时)搅局效应严重
3. BBR:Google 的"模型派"搅局者
BBR(Bottleneck Bandwidth and RTT)于 2016 年由 Google 发布,2017 年合入 Linux 4.9。它彻底抛弃了"丢包=拥塞"的信令,转而测量网络模型参数:
发送速率 = BtlBW(瓶颈带宽)× 匹配系数
目标:以 BtlBW 的精确估计值发送,且 in-flight ≈ BDP(带宽延迟积)
核心探测循环(8 阶段状态机)
BBR 每 8 个 RTT 为一个周期循环执行:
| 阶段 | 持续时间 | 行为 | 目的 |
|---|---|---|---|
| STARTUP | DRain 判定退出 | 指数增窗(类似慢启动) | 快速填满管道 |
| DRAIN | 1 RTT | 指数减窗,排空 STARTUP 时的多余队列 | 消除 startup bufferbloat |
| PROBE_BW | 6 RTT | 交替执行 pacing_gain:1.25→0.75→1→1→1→1 | 周期性探测更高带宽、排空剩余队列 |
| PROBE_RTT | 持续 200ms(最小值) | 窗口压到 4 个包(极小) | 更新 RTT 估计 |
BBR 的 pacing_gain 是关键创新:每个阶段用一个增益系数乘以 BtlBW 去发包。1.25x 时多发包探测带宽富余,0.75x 时少发包排空之前可能产生的排队,恰好等于 1.0x 时稳定传输。
BBRv2:为解决公平性和丢包场景
BBR 在丢包率 >10% 的 WiFi/蜂窝网络下表现不如预期,BBRv2 增加了:
- 基于丢包的快速退出:丢包率 > 2% 时立即进入 PROBE_RTT
- ECN 响应:支持 CE 标记信号
- 更快的收敛:调整增益周期
4. 算法选择决策矩阵
生产环境选择TCP拥塞控制算法时,需要根据具体的网络特征和服务类型来做权衡。不同的部署场景对吞吐量、延迟、丢包容忍度的要求各不相同,比如跨洋Replication需要高带宽利用且对延迟波动容忍度高,而Video Streaming则需要在吞吐和延迟之间取得平衡。通过分析内核版本、网络环境和应用SLA,可以确定最适合的算法配置。
对于已经升级到6.0以上的内核,BBRv2通常是推荐的默认选择,除非处于丢包率较高的环境中,这时CUBIC可能更稳定。监控这些场景时,需要关注丢包率、队列深度和ECN信号等关键指标,以保证算法选择的合理性。
5. 生产部署实战:sysctl 调优链
5.1 切换算法
# 查看可用算法(内核已编译支持的)
sysctl net.ipv4.tcp_available_congestion_control
# 典型输出: cubic reno bbr
# 全局切换为 BBR
sysctl -w net.ipv4.tcp_congestion_control=bbr
# 验证当前生效
sysctl net.ipv4.tcp_congestion_control
ss -ti dst 8.8.8.8 | head -5
# 看输出里的 cubic/bbr 标识
内核编译要求:BBR 需要 CONFIG_TCP_CONG_BBR=m 和 CONFIG_DEFAULT_BBR=y。CentOS 8+、Ubuntu 20.04+、Debian 11+ 通常默认开启。老内核通过 DKMS 安装 tcp_bbr 模块:
# 检查模块是否可加载
modprobe tcp_bbr
lsmod | grep bbr
5.2 BBR 关键调优参数
BBR 不需要像 CUBIC 那样大量调参数,但几个关键点:
# 1. 确保 Pacing 开启(BBR 强依赖 FQ 或内核 pacing)
sysctl -w net.core.default_qdisc=fq
# fq 下 BBR 直接受内核调度;fq_codel + BBR 组合也常见但需注意
# 2. 接收窗口自动调谐(避免瓶颈不在拥塞窗口而在接收端)
sysctl -w net.ipv4.tcp_mtu_probing=1 # MTU 探测
sysctl -w net.ipv4.tcp_base_mss=1024 # 探测基准
# 3. 发送缓冲区(高 BDP 网络需要)
sysctl -w net.ipv4.tcp_wmem="4096 131072 16777216" # min default max
sysctl -w net.core.wmem_max=16777216
5.3 CUBIC 调优
CUBIC 在 BBR 不适用的场景仍然核心,调优要点:
# 1. 开启 HyStart++(减少 CUBIC 的 bufferbloat 效应)
sysctl -w net.ipv4.tcp_cubic_binary=1
# 或者内核 5.17+ 默认的 HyStart++
# 2. ECN(显式拥塞通知)配合 CUBIC
sysctl -w net.ipv4.tcp_ecn=1 # 1=请求ECN, 2=仅接受
# 在数据中心内网强烈建议开启,把"拥塞信号"提前到队列填充之前
# 3. 快速恢复相关
sysctl -w net.ipv4.tcp_recovery=1 # RACK 重传检测(推荐)
sysctl -w net.ipv4.tcp_early_retrans=3
6. 监控与诊断:看得见才调得动
6.1 实时连接状态:ss -ti
ss -tin 'dst :443' | head -20
输出关键字段解析:
cubic(cwnd:10, ssthresh:26) retrans:0/182 ...
cwnd:当前拥塞窗口(MSS 个数)ssthresh:慢启动阈值rtt:平滑 RTT(格式 56.7/12.3 分别是 rtt 和 rttvar)bytes_sent / bytes_acked:可推算带宽
BBR 连接则显示:
bbr(bw:42.2Mbps, mrtt:42.1, pacing_rate:63.3Mbps, ...)
bw:估计瓶颈带宽(核心指标!)mrtt:最小 RTT(过去 10s 内的最小值)pacing_rate:实际发送速率
6.2 脚本化监控
#!/bin/bash
# bbr_monitor.sh — 连续采样某连接的 BBR 核心指标
TARGET_HOST="${1:-8.8.8.8}"
TARGET_PORT="${2:-443}"
while true; do
line=$(ss -ti dst ${TARGET_HOST}:${TARGET_PORT} 2>/dev/null | grep -A1 "^State ")
if [[ -z "$line" ]]; then
echo "$(date '+%H:%M:%S') NO_CONNECTION"
sleep 5; continue
fi
# 提取 bbr 字段
if echo "$line" | grep -q "bbr"; then
bw=$(echo "$line" | grep -oP 'bw:\K[0-9.]+[GMK]?bps')
mrtt=$(echo "$line" | grep -oP 'mrtt:\K[0-9.]+')
rate=$(echo "$line" | grep -oP 'pacing_rate:\K[0-9.]+[GMK]?bps')
cwnd=$(echo "$line" | grep -oP 'cwnd:\K[0-9]+')
retrans=$(echo "$line" | grep -oP 'retrans:\K[0-9/]+')
echo "$(date '+%H:%M:%S') alg=BBR bw=${bw} mrtt=${mrtt}ms pacing=${rate} cwnd=${cwnd} retrans=${retrans}"
else
cwnd=$(echo "$line" | grep -oP 'cwnd:\K[0-9]+')
ssthresh=$(echo "$line" | grep -oP 'ssthresh:\K[0-9]+')
rtt=$(echo "$line" | grep -oP 'rtt:\K[0-9.]+/[0-9.]+')
retrans=$(echo "$line" | grep -oP 'retrans:\K[0-9/]+')
echo "$(date '+%H:%M:%S') alg=CUBIC cwnd=${cwnd} ssthresh=${ssthresh} rtt=${rtt} retrans=${retrans}"
fi
sleep 3
done
6.3 bpftrace 深钻:BBR 转换阶段追踪
#!/usr/bin/bpftrace
// trace_bbr_phase.bt — 追踪 BBR 状态机切换
kprobe:bbr_set_state
{
$sk = (struct sock *)arg0;
$new_state = arg1;
@states[pid, $sk] = $new_state;
$new_text = $new_state == 0 ? "Startup" :
$new_state == 1 ? "Drain" :
$new_state == 2 ? "ProbeBW" :
$new_state == 3 ? "ProbeRTT" : "Unknown";
printf("BBR pid=%d sk=0x%px -> %s\n", pid, $sk, $new_text);
}
kprobe:bbr_set_pacing_gain
{
$sk = (struct sock *)arg0;
$gain = (u32)arg1;
// gain 编码为 8 进制:156/128 = 1.218,128/128 = 1,96/128 = 0.75
$gain_f = $gain / 128.0;
printf("BBR pid=%d pacing_gain=%.3f\n", pid, $gain_f);
}
使用方法:
sudo bpftrace trace_bbr_phase.bt -c 'curl https://your-service/test.bin -o /dev/null'
7. ECN+BBR:现代数据中心的最佳搭档
ECN(Explicit Congestion Notification,RFC 3168)允许路由器在丢包前通过 CE 标记发出拥塞信号。BBR 从 v2 开始原生响应 CE 标记:
# 启用 ECN(主动请求 + 被动响应)
sysctl -w net.ipv4.tcp_ecn=2
# 验证:看 ss -ti 里的 ecnseen 是否 >0
# 或者用 tcpdump 抓:
# tcpdump -i any 'tcp[13] & 0xc0 != 0' # 抓 CE 标记的帧
ECN+BBR 组合的效果:
- 在传统"会丢包"的拥塞点,ECN 提前触发响应——不丢包
- BBR 的 ProbeRTT/probeBW 循环不被频繁的丢包事件打断
- 数据中心的 RTT 抖动可降低 60%+
8. 案例:一只 HDFS DataNode 的 BBR 升级
某次将离线计算集群 DataNode 从 CUBIC 切到 BBR 的观察:
| 指标 | CUBIC | BBR | 变化 |
|---|---|---|---|
| 平均吞吐 | 620Mbps | 922Mbps | +48.7% |
| P99 延迟 | 218ms | 71ms | -67% |
| 丢包率(iperf 测) | 3.8% | 0.02% | -99% |
| CPU(网络栈部分) | 28% | 32% | +14% |
关键验证步骤:
- 先用
iperf3 -c target -t 60在基线下测定丢包率和带宽 - 切换 BBR + 重启 HDFS DataNode
- 观察 30 分钟后对比 MapReduce 作业的 Reduce 阶段时间
- 监控
nvidia-smi(如果有 GPU shuffle)或者网卡计数器ethtool -S
注意事项:BBR 初期会"抢占"更多带宽——如果你的硬件交换机不支持足够大的缓冲,CUBIC 连接可能被饿死。逐步灰度是正道。
9. 算法选择的黄金法则
- BBR 首选:所有延迟敏感服务(API 网关、负载均衡器、实时消息)+ 长肥管道(跨国专线、跨洋 Replication)
- CUBIC 保留:丢包率 > 2% 的不可靠网络、IoT 设备、卫星链路、5G 弱信号场景
- 加 ECN:所有数据中心内网,无论 BBR 还是 CUBIC
- 内核选择:5.15+ 内核的 BBRv2 比 4.x 的 BBRv1 成熟得多,越新越好
升级到 BBR 不只是改一行 sysctl,它需要配合 pacing、microkernel 调度、ECN 一起调校。真正的性能调优最终是一个系统工程——算法只是起点。

发表评论 取消回复