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%

关键验证步骤:

  1. 先用 iperf3 -c target -t 60 在基线下测定丢包率和带宽
  2. 切换 BBR + 重启 HDFS DataNode
  3. 观察 30 分钟后对比 MapReduce 作业的 Reduce 阶段时间
  4. 监控 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 一起调校。真正的性能调优最终是一个系统工程——算法只是起点。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.363881s