Linux TCP拥塞控制算法CUBIC与BBR内核实现对比

📅 2026/7/21 18:15:21
Linux TCP拥塞控制算法CUBIC与BBR内核实现对比
Linux TCP拥塞控制算法CUBIC与BBR内核实现对比TCP拥塞控制是Linux内核net/ipv4/目录下的核心模块通过struct tcp_congestion_ops接口统一抽象。CUBICnet/ipv4/tcp_cubic.c与BBRnet/ipv4/tcp_bbr.c代表了两种截然不同的设计哲学基于丢包模型的窗口调整与基于带宽和RTT测量的 pacing 模型。本文从内核源码层面拆解两者的关键路径。c// include/net/tcp.hstruct tcp_congestion_ops {struct list_head list;u32 key;u32 flags;char name[TCP_CA_NAME_MAX];int (*init)(struct sock *sk);void (*release)(struct sock *sk);void (*cong_control)(struct sock *sk, const struct rate_sample *rs);void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);void (*ssthresh)(struct sock *sk);void (*undo_cwnd)(struct sock *sk);u32 (*tcp_reno_ssthresh)(struct sock *sk);u32 (*undo_cwnd)(struct sock *sk);u32 (*min_tso_segs)(struct sock *sk);struct tcp_congestion_ops *to_fastretrans_alt;...};CUBIC通过cong_avoid挂入主路径BBR则通过cong_control使用rate_sample输入。这个差异本身就暴露了设计分歧CUBIC只在ACK到达时决策BBR在每个RTT内的多个时间点都可以调整。CUBIC的核心状态机由bictcp结构体管理关键字段包括epoch_start、origin_point、last_max_cwnd、bic_scale和tcp_friendliness。c// net/ipv4/tcp_cubic.cstruct bictcp {u32 cnt; /* cwnd增长步长倒数 */u32 last_max_cwnd; /* 上次窗口最大值用于W_max跟踪 */u32 loss_cwnd; /* 上次丢包时的cwnd */u32 last_cwnd; /* 上次bic_update期间计算的cwnd */u32 epoch_start; /* 当前拥塞避免阶段的开始时间戳 */u32 origin_point; /* W_max减去窗口下降量 */u32 bic_scale; /* 影响三次函数曲率的缩放因子 */u8 delay_min; /* 最小RTT用于hybrid slow start */u32 ack_cnt; /* 累计ACK计数 */...};CUBIC在丢包发生后调用tcp_cubic_ssthresh将ssthresh设为当前cwnd的β倍默认717 0.7实际用右移实现近似。关键在恢复阶段结束后cwnd重新进入拥塞避免时调用bictcp_cong_avoid它使用一个三次函数计算目标窗口c// net/ipv4/tcp_cubic.c -- bictcp_cong_avoidstatic void bictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked){struct tcp_sock *tp tcp_sk(sk);struct bictcp *ca inet_csk_ca(sk);if (!tcp_is_cwnd_limited(sk))return;if (tcp_in_slow_start(tp)) {if (hystart after(tp-snd_una, ca-end_seq))bictcp_hystart_reset(sk);acked tcp_slow_start(tp, acked);if (!acked)return;}bictcp_update(ca, tp-snd_cwnd, tcp_ca_dst_ecn_ecn(sk));tcp_cong_avoid_ai(tp, ca-cnt, acked);}这里注意到tcp_is_cwnd_limited检查。如果发送方不受cwnd限制比如受应用层或pacing限制CUBIC不会增加窗口。这是很多人在读CUBIC代码时忽略的边界条件当SO_MAX_PACING_RATE设置较低导致实际发送速率低于cwnd允许值时CUBIC实际上不会增长cwnd所以从BBR切换到CUBIC时不会出现窗口激增。bictcp_update函数实现了三次函数的核心计算c// net/ipv4/tcp_cubic.c -- bictcp_updatestatic inline void bictcp_update(struct bictcp *ca, u32 cwnd, u32 ecn){u32 delta, bic_target, offs;u64 t, log_cnt;ca-ack_cnt;if (ca-epoch_start 0) {ca-epoch_start tcp_jiffies32;ca-ack_cnt 1;ca-tcp_cnt 0;ca-last_max_cwnd cwnd;ca-bic_scale 8; /* 默认BIC缩放因子 */}/* 三次函数参数t elapsed time / minRTT */t ((tcp_jiffies32 - ca-epoch_start) 3) / HZ;t max(t, 1ULL);/* 计算W(t) W_max - beta * W_max C * (t - K)^3 */offs ca-last_max_cwnd - cwnd;if (offs 0)offs 0;/* K cubic_root(W_max * beta / C) */log_cnt (u64)ca-last_max_cwnd * 717 / 1024;log_cnt (log_cnt 5) / ca-bic_scale;...}三次函数的凹区域和凸区域切换点在K时间点。当t K时函数处于凹增长区域增速加快当t K时进入凸区域增速减慢。但内核实现中为了稳定性在bictcp_update里对cnt做了clampcnt最小值不能低于2这防止了cwnd在每个ACK上增长超过1个MSS。这对应着tcp_cong_avoid_ai中的加法增加逻辑。现在对比BBR。BBR的状态机完全不依赖丢包事件而是通过pacing_rate和cwnd_gain两个参数控制发送c// net/ipv4/tcp_bbr.cstruct bbr {u32 lt_use_bw; /* 是否使用带宽滤波器的长期值 */u32 bw_lo; /* 带宽下界用于probe RTT */u32 bw_hi; /* 带宽上界max bw filter输出 */u32 rtt_cnt; /* 当前阶段的RTT计数 */u32 next_round_delivered; /* 用于轮次追踪的delivered标记 */struct {u32 bw; /* 带宽单位bps */u32 rtt; /* 最小RTT单位us */} max_bw_filter[2]; /* 带宽窗滤波器 */struct {u32 bw; /* 窗口内的最大带宽 */u32 rtt; /* 窗口内的最小RTT */} win;u8 state; /* BBR状态STARTUP, DRAIN, PROBE_BW, PROBE_RTT */u8 mode; /* pacing_gain和cwnd_gain模式 */...};BBR的核心更新函数是bbr_main挂载在cong_control回调中。每收到一个ACK时内核会计算rate_sample并传入c// net/ipv4/tcp_bbr.c -- bbr_mainstatic void bbr_main(struct sock *sk, const struct rate_sample *rs){struct bbr *bbr inet_csk_ca(sk);if (!bbr-initialized)bbr_init(sk);bbr_update_model(sk, rs);bbr_update_gains(sk);bbr_update_pacing_rate(sk);bbr_update_cwnd(sk);bbr_update_ack_aggregation(sk);}bbr_update_model包含两条并行的滤波器路径最大带宽的max滤波器和最小RTT的min滤波器。带宽滤波器是BBR最微妙的设计它的窗口长度由bbr_bw_rtts决定单位是RTT轮次而非时间绝对值c// net/ipv4/tcp_bbr.c -- bbr_update_bwstatic void bbr_update_bw(struct sock *sk, const struct rate_sample *rs){struct bbr *bbr inet_csk_ca(sk);u32 bw rs-delivered * 1000 / (rs-interval_us ? : 1);u64 bw_usecs (u64)bw * rs-interval_us;if (!rs-acked_sacked || rs-interval_us 0)return;/* 窗口滤波维护最近N个RTT轮次内的最大带宽 */bbr-max_bw_filter[bbr-id] max(bbr-max_bw_filter[bbr-id], bw);if (bbr-rounds_since_bw_high 0) {bbr-rounds_since_bw_high--;} else {bbr-id ^ 1;bbr-rounds_since_bw_high 1;}}这里一个关键边界是rs-interval_us 0的保护。当TSval时间戳回绕或者接收端ACK没有携带有用时间信息时测量到的interval_us可能为0或负值此时除零会导致内核崩溃。另一个边界是bbr-rounds_since_bw_high的减法它可能下溢——但bbr_init中初始化它为0而bbr_update_bw只在bbr-rounds_since_bw_high递减到0时切换滤波器桶所以不会出现负数。但需要明确如果某个RTT内没有收到任何ACK即bbr_main没有被调用滤波器不会更新这可能导致旧带宽值在窗口中驻留过久当发送链路突然变差时BBR需要至少一个完整RTT才能响应。BBR的pacing_rate计算直接控制发送节奏c// net/ipv4/tcp_bbr.c -- bbr_set_pacing_ratestatic void bbr_set_pacing_rate(struct sock *sk){struct bbr *bbr inet_csk_ca(sk);u32 rate bbr-max_bw_filter[bbr-id];/* 应用pacing_gain */rate (u64)rate * bbr-pacing_gain BBR_SCALE;/* 应用probe_rtt状态的带宽下限 */if (bbr-state BBR_PROBE_RTT)rate min_t(u32, rate, bbr-bw_lo);/* 最终pacing rate不能超过应用设置的MAX_PACING_RATE */rate min_t(u32, rate, sk-sk_max_pacing_rate);/* 写入内核pacing引擎 */sk-sk_pacing_rate rate;}这里有一个重要的竞态场景sk_pacing_rate的写入与tcp_write_xmit中的pacing FQfair queueing调度器读取不是原子操作。当32-bit rate赋值被拆分成两个16-bit写入时pacing调度器可能读到撕裂的中间值导致短时间的rate尖峰或低谷。内核通过将sk_pacing_rate定义为atomic_t解决了这个问题4.19内核已修复。BBR的cwnd计算使用bbr_quantization_budget将pacing_rate乘以RTT得到BDP再乘以cwnd_gainc// net/ipv4/tcp_bbr.c -- bbr_update_cwndstatic void bbr_update_cwnd(struct sock *sk){struct bbr *bbr inet_csk_ca(sk);u32 cwnd 0, target_cwnd 0;/* 目标cwnd BDP * cwnd_gain */target_cwnd (u64)bbr-max_bw_filter[bbr-id] *bbr-min_rtt_us * bbr-cwnd_gain BBR_SCALE;/* 转换为MSS单位 */target_cwnd bbr_quantization_budget(sk, target_cwnd, 0);/* 保证cwnd至少为4个MSSRFC要求 */target_cwnd max_t(u32, target_cwnd, 4);if (bbr-state BBR_PROBE_RTT) {cwnd bbr_probe_rtt_cwnd(sk);} else {cwnd max(target_cwnd, bbr-prior_cwnd);cwnd min(cwnd, bbr-max_cwnd);}tp-snd_cwnd cwnd;}target_cwnd的计算使用了bbr-max_bw_filter[bbr-id]和bbr-min_rtt_us。这里有一个精度问题当RTT非常小例如本地回环测试中RTT 10us时min_rtt_us取整导致BDP计算偏差巨大。BBR在bbr_init中强制min_rtt_us的最小值为10usBBR_MIN_RTT来缓解c// net/ipv4/tcp_bbr.c -- bbr_init#define BBR_MIN_RTT 10 /* 最小RTT边界10微秒 */static void bbr_init(struct sock *sk){struct bbr *bbr inet_csk_ca(sk);bbr-min_rtt_us tcp_min_rtt(sk);if (bbr-min_rtt_us ~0U)bbr-min_rtt_us BBR_MIN_RTT;bbr-min_rtt_stamp tcp_jiffies32;...}tcp_min_rtt返回的是TCP层维护的平滑最小RTT如果从未采样到则返回~0U所以BBR必须做这个检查。但如果连接在首次RTT采样前就发了大量数据实际上tcp_min_rtt在SYN-ACK握手阶段就已经被初始化了所以生产环境下不会触发这个fallback。CUBIC和BBR最本质的差异体现在丢包响应上。CUBIC在tcp_cubic_ssthresh中直接将cwnd乘以βc// net/ipv4/tcp_cubic.cstatic u32 tcp_cubic_ssthresh(struct sock *sk){const struct tcp_sock *tp tcp_sk(sk);struct bictcp *ca inet_csk_ca(sk);ca-loss_cwnd tp-snd_cwnd;ca-last_max_cwnd tp-snd_cwnd;/* β 717/1024 ≈ 0.7通过右移避免浮点数 */return max((tp-snd_cwnd * 717) 10, 2U);}而BBR完全忽略丢包对cwnd的直接缩减c// net/ipv4/tcp_bbr.cstatic void bbr_state_loss(struct sock *sk){struct bbr *bbr inet_csk_ca(sk);/* BBR在丢包时不下降cwnd只重置带宽滤波器 */bbr-lt_use_bw 0;bbr-bw_lo ~0U;/* 如果正处于STARTUP阶段丢包导致进入DRAIN */if (bbr-state BBR_STARTUP)bbr_set_state(sk, BBR_DRAIN);/* 进入PROBE_RTT以重新校准min_rtt */bbr-probe_rtt_done_stamp 0;}当CUBIC遇到持续丢包时cwnd随着每次丢包事件不断收缩在浅缓存链路中可能收敛到2个MSS的ssthresh下限。BBR则只靠带宽滤波器感知链路变化——如果丢包没有引起带宽下降例如路由器队列尾丢弃但链路速率不变BBR会维持原有发送速率导致持续丢包。这在BBRv1中是个已知缺陷也是BBRv3引入ECN和丢包率阈值的原因。CUBIC还有一个隐藏的竞态问题当CA_ACK_FAST_PATH被触发时部分ACK处理会绕过cong_avoid路径。查看net/ipv4/tcp_input.c中tcp_ack的fast path逻辑c// net/ipv4/tcp_input.cstatic int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag){...if (flag FLAG_CA_ALERT)tcp_process_cong_alert(sk, ack, flag); /* 进入cong_control路径 */...if (tcp_ack_is_dubious(sk, flag)) {...} else {/* Fast path: 直接进入CA_EVENT_FAST_ACK事件处理 */tcp_ca_event(sk, CA_EVENT_FAST_ACK);/* 如果icsk_ca_ops-cong_avoid存在则调用 */if (icsk-icsk_ca_ops-cong_avoid)icsk-icsk_ca_ops-cong_avoid(sk, ack, acked);}...}这里的边界是当tcp_ack_is_dubious返回真时例如接收到DUPACK或SACKed序列号异常进入拥塞响应路径可能会调用tcp_fastretrans_alert直接修改cwnd。如果此时CUBIC的bictcp_update刚刚基于旧cwnd计算了cnt下一次ACK使用这个陈旧的cnt可能导致过度增长。不过CUBIC用ca-ack_cnt和epoch_start规避了这个问题——每次bictcp_update都会基于实时cwnd重新计算。BBR的pacing引擎依赖内核的FQFair Queueingqdisc或TSOTCP Segmentation Offload的burst机制。当TSO开启时内核在tcp_tso_should_defer中判断是否需要延迟发送以匹配pacing ratec// net/ipv4/tcp_output.cstatic bool tcp_tso_should_defer(struct sock *sk, struct sk_buff *skb,bool *is_cwnd_limited, u32 *max_segs){const struct tcp_sock *tp tcp_sk(sk);u32 send_forward, hint;...if (tp-tcp_mstamp - tp-tcp_wstamp_max 0)return true; /* 上次发送的时间戳还在pacing interval内推迟发送 *//* 计算下一个TSO burst可以发送多少个segments */...}这里tp-tcp_wstamp_max - tp-tcp_mstamp的减法用u32计算如果时间戳回绕会导致误判。内核在tcp_mstamp_refresh中维护了时间戳单调递增性来防止回绕。但如果连接长时间静默超过49.7天的jiffies回绕周期pacing间隔计算会出错BBR可能短时间内爆发大量数据包——好在这种场景在现实网络中几乎不可能出现。另一个值得深挖的边界是CUBIC的hybrid slow starthystart。它在tcp_cubic.c中通过检测ACK间隔ACK train和RTT增长来退出慢启动而不是等丢包。hystart的检测逻辑c// net/ipv4/tcp_cubic.cstatic void hystart_update(struct sock *sk, u32 delay){struct tcp_sock *tp tcp_sk(sk);struct bictcp *ca inet_csk_ca(sk);if (!(ca-found HYSTART_ACKTRAIN)) {/* ACK train检测如果连续ACK到达间隔小于delay_min/2认为在慢启动 */if (ca-last_ack_delta ca-delay_min 3)ca-found | HYSTART_ACKTRAIN;}if (!(ca-found HYSTART_DELAY)) {/* RTT增长检测如果当前RTT超过最小RTT的阈值退出 */if (delay ca-delay_min 1)ca-found | HYSTART_DELAY;}if (ca-found HYSTART_ACKTRAIN || ca-found HYSTART_DELAY)ca-found | HYSTART_START; /* 准备退出慢启动 */}delay_min 3和delay_min 1都是经验值没有理论保证。在噪声较大的无线网络中RTT抖动可能导致hystart提前退出cwnd在慢启动阶段只增长到几十个MSS极大影响短流性能。这可以通过tcp_cubic.c模块参数hystart_detect关闭。性能影响方面CUBIC的bictcp_update包含64位乘法运算(u64)ca-last_max_cwnd * 717 / 1024在每次ACK到达时执行。对于万兆网卡每秒数万个ACK的场景这个乘法的CPU开销不可忽视。BBR的bbr_main中的计算虽然更多带宽滤波、pacing rate、cwnd但由于使用了移位和条件赋值而非64位除法实际指令数更少。但BBR需要FQ qdisc配合才能发挥效果——在没有fq_codel的场景下仅靠TSO/GSO的burst发送会破坏pacing的精度导致队列堆积和bufferbloat。BBR的probe RTT状态BBR_PROBE_RTT设计为每10秒BBR_PROBE_RTT_INTERVAL进入一次将cwnd缩减为4个MSS持续至少200msBBR_PROBE_RTT_MIN_MS来清空网络管道并重新测量最小RTT。但实现中存在一个重要的边界如果min_rtt_us_floor被错误更新probe RTT的退出条件可能永远不满足c// net/ipv4/tcp_bbr.c -- bbr_update_min_rttstatic void bbr_update_min_rtt(struct sock *sk, const struct rate_sample *rs){struct bbr *bbr inet_csk_ca(sk);u32 min_rtt_us rs-rtt_us;if (rs-rtt_us rs-rtt_us bbr-min_rtt_us) {bbr-min_rtt_us rs-rtt_us;bbr-min_rtt_stamp tcp_jiffies32;}/* probe RTT超时后只有min_rtt_us更新了才退出 */if (bbr-probe_rtt_done_stamp after(tcp_jiffies32, bbr-probe_rtt_done_stamp))bbr-probe_rtt_done_stamp 0;}如果链路的实际最小RTT在probe RTT期间没有改善比如receiver端的处理延迟主导了RTTmin_rtt_us不会下降但probe_rtt_done_stamp的超时机制仍然会让BBR退出PROBE_RTT状态。所以这个问题在超时机制上不会死锁但在min_rtt_us得不到更新的情况下后续的BDP计算会使用一个偏大的RTT值导致cwnd被高估。CUBIC和BBR的共存问题是多流竞争时的核心关注点。在同一个瓶颈链路中CUBIC流遇到丢包会立刻降窗BBR流则不受影响——这导致CUBIC流获得的带宽远低于BBR流。内核在tcp_register_congestion_control层面提供了公平性钩子但没有对算法间的竞争做任何干预c// net/ipv4/tcp_cong.cint tcp_register_congestion_control(struct tcp_congestion_ops *ca){int ret 0;spin_lock(tcp_cong_list_lock);if (ca-key ! ~0U) {ca-key tcp_cong_control_key(ca-name, ca-name_len);if (ca-key ~0U) {pr_err(TCP: %s key collision\n, ca-name);ret -EEXIST;goto out;}}list_add_tail_rcu(ca-list, tcp_cong_list);...}RTT不公平性是另一个根深蒂固的问题。CUBIC的cwnd增速与RTT的三次方成反比因为三次函数的时间轴以RTT为单位所以RTT小的流增长更快获得更多带宽。BBR在STARTUP阶段使用pacing_gain2.89BBR_HIGH_GAIN 2885 / BBR_SCALEcwnd_gain2理论上与RTT无关——但实际中RTT影响采样轮次切换频率间接影响带宽滤波器的更新粒度导致短RTT流仍然占优。最后两者在undo路径上的差异也值得注意。当发生虚假重传如reordering或DSACK时CUBIC调用tcp_cubic_undo_cwnd恢复丢包前的cwndc// net/ipv4/tcp_cubic.cstatic u32 tcp_cubic_undo_cwnd(struct sock *sk){const struct tcp_sock *tp tcp_sk(sk);struct bictcp *ca inet_csk_ca(sk);ca-last_max_cwnd tp-snd_cwnd; // 恢复旧值return max(tp-snd_cwnd, ca-loss_cwnd);}BBR的undo实现是空操作c// net/ipv4/tcp_bbr.cstatic u32 bbr_undo_cwnd(struct sock *sk){struct bbr *bbr inet_csk_ca(sk);return bbr-prior_cwnd;}BBR在虚假重传后不恢复任何状态因为它的模型不依赖丢包来维持cwnd。但prior_cwnd可能已经被后续的带宽更新覆盖所以undo后可能仍然低于应有值。好在BBR在接下来几个RTT内会通过带宽滤波器快速恢复到正确速率所以这在实际中影响不大。从代码行数看tcp_cubic.c约650行tcp_bbr.c约1200行。BBR的复杂度来自其模型维护和状态机管理。但CUBIC的数学计算虽然代码量少其三次函数中K值的开方运算基于二分查找近似cubic_root在极端cwnd下如超过10万MSS的数据中心链路存在收敛慢的问题PATCH版本如tcp_cubic_fast通过查表法优化了这一路径。