TCP 拥塞控制是互联网稳定运行的核心机制之一。自 1988 年 Van Jacobson 在 SIGCOMM 发表奠基论文以来,拥塞控制算法经历了从 Tahoe、Reno、BIC 到 CUBIC,再到基于测量的 BBR 的演进历程。本文以 Linux 6.4-rc1 源码为基础,深入剖析内核拥塞控制框架的实现细节,覆盖从插件注册到状态机转换、从 CUBIC 三次函数到 BBR 带宽估计的完整链路。
拥塞控制算法面对的核心挑战是:如何在不知道网络容量的前提下,以尽可能高的速率发送数据,同时不引发严重拥塞?这个问题的本质是分布式资源竞争——互联网中每条流都是自私的,拥塞控制协议需要在个体利益(最大化自身吞吐)和集体利益(网络整体公平稳定)之间取得平衡。Jacobson 的原始方案用丢包作为拥塞的唯一信号,这在当时(网络带宽以 Kbps 计)是合理的。但随着网络带宽扩展到 Gbps、Tbps 量级,以及 bufferbloat(路由器缓冲区过大导致的延迟膨胀)问题的出现,基于丢包的方案暴露出越来越多的局限性,催生了 CUBIC 和 BBR 等新一代算法的诞生。
一、拥塞控制框架:可插拔的算法架构 1.1 操作集接口:struct tcp_congestion_ops Linux 内核将拥塞控制设计为可插拔模块。每种算法通过实现 struct tcp_congestion_ops 接口注册到内核,该结构体定义在 include/net/tcp.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 struct tcp_congestion_ops { 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); void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample); u32 (*min_tso_segs)(struct sock *sk); void (*cong_control)(struct sock *sk, const struct rate_sample *rs); u32 (*undo_cwnd)(struct sock *sk); size_t (*get_info)(struct sock *sk, u32 ext, int *attr, union tcp_cc_info *info); char name[TCP_CA_NAME_MAX]; struct module *owner ; struct list_head list ; u32 key; u32 flags; void (*init)(struct sock *sk); void (*release)(struct sock *sk); } ____cacheline_aligned_in_smp;
结构体中有三个回调是必须实现 的(required):
ssthresh:丢包后计算新的慢启动阈值
cong_avoid 或 cong_control:实现拥塞避免逻辑(二选一)
undo_cwnd:虚假拥塞撤销时恢复窗口
tcp_validate_congestion_control 在注册时强制校验这三个字段:
1 2 3 4 5 6 7 8 9 10 11 int tcp_validate_congestion_control (struct tcp_congestion_ops *ca) { if (!ca->ssthresh || !ca->undo_cwnd || !(ca->cong_avoid || ca->cong_control)) { pr_err("%s does not implement required ops\n" , ca->name); return -EINVAL; } return 0 ; }
结构体被标记为 ____cacheline_aligned_in_smp,确保在 SMP 环境下 fast path 字段(ssthresh、cong_avoid 等)落在同一 cache line,减少 false sharing。
1.2 算法注册与注销 算法通过 tcp_register_congestion_control 注册,使用全局链表 tcp_cong_list 管理。注册时用 jhash 计算名称的哈希值作为唯一 key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int tcp_register_congestion_control (struct tcp_congestion_ops *ca) { int ret; ret = tcp_validate_congestion_control(ca); if (ret) return ret; ca->key = jhash(ca->name, sizeof (ca->name), strlen (ca->name)); spin_lock(&tcp_cong_list_lock); if (ca->key == TCP_CA_UNSPEC || tcp_ca_find_key(ca->key)) { pr_notice("%s already registered or non-unique key\n" , ca->name); ret = -EEXIST; } else { list_add_tail_rcu(&ca->list , &tcp_cong_list); pr_debug("%s registered\n" , ca->name); } spin_unlock(&tcp_cong_list_lock); return ret; } EXPORT_SYMBOL_GPL(tcp_register_congestion_control);
注销时使用 synchronize_rcu() 等待所有 RCU reader 完成,保证模块安全卸载:
1 2 3 4 5 6 7 void tcp_unregister_congestion_control (struct tcp_congestion_ops *ca) { spin_lock(&tcp_cong_list_lock); list_del_rcu(&ca->list ); spin_unlock(&tcp_cong_list_lock); synchronize_rcu(); }
1.3 初始化流程:tcp_init_congestion_control 每个新连接建立时,tcp_assign_congestion_control 从 per-net namespace 的默认配置中读取算法,随后在连接完成三次握手后调用 tcp_init_congestion_control:
1 2 3 4 5 6 7 8 9 10 11 12 13 void tcp_init_congestion_control (struct sock *sk) { struct inet_connection_sock *icsk = inet_csk(sk); tcp_sk(sk)->prior_ssthresh = 0 ; if (icsk->icsk_ca_ops->init) icsk->icsk_ca_ops->init(sk); if (tcp_ca_needs_ecn(sk)) INET_ECN_xmit(sk); else INET_ECN_dontxmit(sk); icsk->icsk_ca_initialized = 1 ; }
算法私有状态存储在 icsk->icsk_ca_priv(固定大小为 ICSK_CA_PRIV_SIZE = 136 字节),避免动态内存分配的开销。
1.4 拥塞状态机 Linux TCP 维护一个五状态机,存储在 icsk->icsk_ca_state:
状态
宏定义
含义
0
TCP_CA_Open
正常传输,无拥塞
1
TCP_CA_Disorder
收到重复 ACK 或 SACK,怀疑乱序或丢包
2
TCP_CA_CWR
收到 ECN 拥塞通知,窗口缩减中
3
TCP_CA_Recovery
确认丢包,快速恢复中
4
TCP_CA_Loss
RTO 超时,进入 Loss 状态
状态转换由 tcp_set_ca_state 完成,该函数同时调用算法的 set_state 钩子,允许算法在状态变化时执行清理动作(如 CUBIC 在进入 Loss 时重置 epoch):
1 2 3 4 5 6 7 8 9 10 void tcp_set_ca_state (struct sock *sk, const u8 ca_state) { struct inet_connection_sock *icsk = inet_csk(sk); trace_tcp_cong_state_set(sk, ca_state); if (icsk->icsk_ca_ops->set_state) icsk->icsk_ca_ops->set_state(sk, ca_state); icsk->icsk_ca_state = ca_state; }
1.5 慢启动与拥塞避免的基础实现 TCP Reno 的慢启动和拥塞避免是所有算法的基础参照。tcp_slow_start 在 net/ipv4/tcp_cong.c 中实现:
1 2 3 4 5 6 7 8 9 __bpf_kfunc u32 tcp_slow_start (struct tcp_sock *tp, u32 acked) { u32 cwnd = min(tcp_snd_cwnd(tp) + acked, tp->snd_ssthresh); acked -= cwnd - tcp_snd_cwnd(tp); tcp_snd_cwnd_set(tp, min(cwnd, tp->snd_cwnd_clamp)); return acked; }
慢启动阶段每收到一个 ACK,cwnd 增加 1,实际效果是每个 RTT 翻倍(指数增长),直到达到 snd_ssthresh。当 snd_ssthresh 被丢包事件设置后,进入拥塞避免阶段,由 tcp_cong_avoid_ai 实现线性增长(每个 RTT 增加 1 个 MSS):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __bpf_kfunc void tcp_cong_avoid_ai (struct tcp_sock *tp, u32 w, u32 acked) { if (tp->snd_cwnd_cnt >= w) { tp->snd_cwnd_cnt = 0 ; tcp_snd_cwnd_set(tp, tcp_snd_cwnd(tp) + 1 ); } tp->snd_cwnd_cnt += acked; if (tp->snd_cwnd_cnt >= w) { u32 delta = tp->snd_cwnd_cnt / w; tp->snd_cwnd_cnt -= delta * w; tcp_snd_cwnd_set(tp, tcp_snd_cwnd(tp) + delta); } tcp_snd_cwnd_set(tp, min(tcp_snd_cwnd(tp), tp->snd_cwnd_clamp)); }
w 参数通常等于当前 cwnd,snd_cwnd_cnt 是 ACK 计数器。当累计 ACK 数达到 w 时才增加 cwnd,实现了 “每个 RTT 增加 1” 的拥塞避免语义,同时通过分数累积避免整数截断导致的增长停滞。
1.6 rate_sample:算法观测的基础数据 BBR 等现代算法依赖 struct rate_sample 提供每个 ACK 时刻的网络状态快照,由 net/ipv4/tcp_rate.c 填充:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct rate_sample { u64 prior_mstamp; u32 prior_delivered; s32 delivered; long interval_us; long rtt_us; int losses; u32 acked_sacked; u32 prior_in_flight; bool is_app_limited; bool is_retrans; bool is_ack_delayed; };
is_app_limited 标志非常重要:若应用发送速率低于网络容量(如写缓冲区为空),则该时段的吞吐样本不能代表网络真实容量,BBR 会在带宽估计时跳过这类样本(除非该样本的带宽不低于当前估计)。
二、CUBIC 算法:Linux 默认拥塞控制 CUBIC 由 Sangtae Ha、Injong Rhee 和 Lisong Xu 设计,2008 年发表于 ACM SIGOPS,是目前 Linux 的默认拥塞控制算法。它用三次函数替代 Reno 的线性增长,在高带宽延迟积(BDP)网络中有更好的利用率。
2.1 核心状态:struct bictcp CUBIC 的算法私有状态定义在 net/ipv4/tcp_cubic.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct bictcp { u32 cnt; u32 last_max_cwnd; u32 last_cwnd; u32 last_time; u32 bic_origin_point; u32 bic_K; u32 delay_min; u32 epoch_start; u32 ack_cnt; u32 tcp_cwnd; u32 round_start; u32 end_seq; u32 last_ack; u32 curr_rtt; };
2.2 三次函数模型与 K 的计算 CUBIC 的窗口增长函数为:
1 W(t) = C * (t - K)^3 + W_max
其中:
W_max:上一次丢包时的拥塞窗口(last_max_cwnd)
C:缩放系数(对应 bic_scale/1024,默认 bic_scale = 41,即 C ≈ 0.04)
K:从当前窗口恢复到 W_max 所需的时间,由以下公式求解:
1 K = cubic_root((W_max - cwnd) * rtt / C)
在 bictcp_update 开始时,若当前处于新 epoch 的起点(epoch_start == 0),则计算 K:
1 2 3 4 5 6 7 8 9 10 if (ca->last_max_cwnd <= cwnd) { ca->bic_K = 0 ; ca->bic_origin_point = cwnd; } else { ca->bic_K = cubic_root(cube_factor * (ca->last_max_cwnd - cwnd)); ca->bic_origin_point = ca->last_max_cwnd; }
cube_factor 在模块初始化时预计算,其值为 2^40 / (bic_scale * 10),将 SRTT 固定为 100ms 时的缩放因子编码进来,避免运行时除法。
cubic_root 使用查表 + 牛顿迭代法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static u32 cubic_root (u64 a) { u32 x, b, shift; static const u8 v[] = { 0 , 54 , 54 , 54 , 118 , 118 , ... }; b = fls64(a); x = (2 * x + (u32)div64_u64(a, (u64)x * (u64)(x - 1 ))); x = ((x * 341 ) >> 10 ); return x; }
2.3 bictcp_update:目标窗口计算核心 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 static inline void bictcp_update (struct bictcp *ca, u32 cwnd, u32 acked) { u32 delta, bic_target, max_cnt; u64 offs, t; ca->ack_cnt += acked; if (ca->last_cwnd == cwnd && (s32)(tcp_jiffies32 - ca->last_time) <= HZ / 32 ) return ; ca->last_cwnd = cwnd; ca->last_time = tcp_jiffies32; t = (s32)(tcp_jiffies32 - ca->epoch_start); t += usecs_to_jiffies(ca->delay_min); t <<= BICTCP_HZ; do_div(t, HZ); if (t < ca->bic_K) offs = ca->bic_K - t; else offs = t - ca->bic_K; delta = (cube_rtt_scale * offs * offs * offs) >> (10 + 3 *BICTCP_HZ); if (t < ca->bic_K) bic_target = ca->bic_origin_point - delta; else bic_target = ca->bic_origin_point + delta; if (bic_target > cwnd) { ca->cnt = cwnd / (bic_target - cwnd); } else { ca->cnt = 100 * cwnd; } tcp_friendliness: if (tcp_friendliness) { u32 scale = beta_scale; delta = (cwnd * scale) >> 3 ; while (ca->ack_cnt > delta) { ca->ack_cnt -= delta; ca->tcp_cwnd++; } if (ca->tcp_cwnd > cwnd) { delta = ca->tcp_cwnd - cwnd; max_cnt = cwnd / delta; if (ca->cnt > max_cnt) ca->cnt = max_cnt; } } ca->cnt = max(ca->cnt, 2U ); }
cnt 的含义是:每收到 cnt 个 ACK 才将 cwnd 增加 1。cnt 越小,窗口增长越快;cnt 越大,增长越保守。
2.4 丢包后的 ssthresh 计算:cubictcp_recalc_ssthresh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __bpf_kfunc static u32 cubictcp_recalc_ssthresh (struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); struct bictcp *ca = inet_csk_ca(sk); ca->epoch_start = 0 ; if (tcp_snd_cwnd(tp) < ca->last_max_cwnd && fast_convergence) ca->last_max_cwnd = (tcp_snd_cwnd(tp) * (BICTCP_BETA_SCALE + beta)) / (2 * BICTCP_BETA_SCALE); else ca->last_max_cwnd = tcp_snd_cwnd(tp); return max((tcp_snd_cwnd(tp) * beta) / BICTCP_BETA_SCALE, 2U ); }
Fast Convergence 机制:当检测到当前窗口比历史最大窗口更小(说明有新流加入竞争),CUBIC 会将 W_max 设得更低,使新旧流更快收敛到公平带宽分配。
2.5 Hystart:慢启动安全退出检测 Hystart 是 CUBIC 集成的混合慢启动算法,目的是在慢启动阶段提前检测到带宽饱和点,避免造成严重队列堆积。它支持两种探测模式(hystart_detect):
1. ACK Train 检测(HYSTART_ACK_TRAIN)
如果一个 RTT 内 ACK 连续密集到达(间隔 <= hystart_ack_delta_us = 2ms),且从 round 开始到现在的时间超过 delay_min 的一半,则认为已达带宽上限,将 snd_ssthresh 设为当前 cwnd。
2. 延迟检测(HYSTART_DELAY)
收集 8 个(HYSTART_MIN_SAMPLES)RTT 样本后,若当前 round 的最小 RTT 比历史最小 RTT 大超过阈值(delay_min >> 3,clamp 到 4ms-16ms 之间),则认为队列开始积累,退出慢启动:
1 2 3 4 5 if (ca->curr_rtt > ca->delay_min + HYSTART_DELAY_THRESH(ca->delay_min >> 3 )) { ca->found = 1 ; tp->snd_ssthresh = tcp_snd_cwnd(tp); }
cubictcp_acked 是 Hystart 的触发入口,在每个 ACK 到达时被调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __bpf_kfunc static void cubictcp_acked (struct sock *sk, const struct ack_sample *sample) { const struct tcp_sock *tp = tcp_sk(sk); struct bictcp *ca = inet_csk_ca(sk); u32 delay; if (sample->rtt_us < 0 ) return ; delay = sample->rtt_us; if (delay == 0 ) delay = 1 ; if (ca->delay_min == 0 || ca->delay_min > delay) ca->delay_min = delay; if (!ca->found && tcp_in_slow_start(tp) && hystart && tcp_snd_cwnd(tp) >= hystart_low_window) hystart_update(sk, delay); }
2.6 CUBIC 的模块初始化与预计算 CUBIC 在模块加载时预计算几个重要的缩放系数,避免运行时除法(net/ipv4/tcp_cubic.c 的 cubictcp_register):
1 2 3 4 5 6 7 8 9 10 11 12 13 beta_scale = 8 *(BICTCP_BETA_SCALE+beta) / 3 / (BICTCP_BETA_SCALE - beta); cube_rtt_scale = (bic_scale * 10 ); cube_factor = 1ull << (10 +3 *BICTCP_HZ); do_div(cube_factor, bic_scale * 10 );
这里的关键设计思想是将 SRTT 固定为 100ms 作为归一化基准来计算 cube_factor,将时间相关的缩放因子从运行时热路径移到模块初始化阶段,使 bictcp_update 中的计算纯粹是整数乘除操作,对 CPU 缓存友好。
CUBIC 的 cubictcp_cong_avoid 是 cong_avoid 接口的实现,先处理慢启动,再调用 bictcp_update 计算 CUBIC 目标窗口,最后通过 tcp_cong_avoid_ai 按照 cnt 速率推进 cwnd:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 __bpf_kfunc static void cubictcp_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)) { acked = tcp_slow_start(tp, acked); if (!acked) return ; } bictcp_update(ca, tcp_snd_cwnd(tp), acked); tcp_cong_avoid_ai(tp, ca->cnt, acked); }
tcp_is_cwnd_limited 检查发送方是否真正被 cwnd 限制(而非被接收窗口或应用层限制),只有在被 cwnd 限制时才增加 cwnd,这防止了在应用层受限时错误地增大窗口。
2.7 为何 CUBIC 优于 Reno 在高带宽延迟积(BDP)网络中,Reno 的线性增长速率 1 MSS/RTT 远远不足。例如,1Gbps 链路、100ms RTT 的路径,BDP = 125,000 包(约 125MB),Reno 从 cwnd=1 恢复到满窗口需要 125,000 个 RTT = 12,500 秒。CUBIC 的三次函数增长在窗口较小时缓慢(避免过激探测),接近 W_max 时快速收敛(高效利用带宽),超过 W_max 后再次加速(主动探测剩余容量),在公平性和带宽利用率之间取得了良好平衡。
三、BBR 算法:基于测量的拥塞控制 BBR(Bottleneck Bandwidth and RTT)由 Google 于 2016 年在 ACM Queue 发表,是一种基于网络模型的拥塞控制算法,核心思想是:不依赖丢包作为拥塞信号,而是持续估计瓶颈带宽(BtlBw)和最小 RTT(RTprop),在最大化吞吐量的同时最小化队列延迟。
文件顶部的注释清晰描述了算法核心:
1 2 3 4 5 On each ACK, update our model of the network path: bottleneck_bandwidth = windowed_max(delivered / elapsed, 10 round trips) min_rtt = windowed_min(rtt, 10 seconds) pacing_rate = pacing_gain * bottleneck_bandwidth cwnd = max(cwnd_gain * bottleneck_bandwidth * min_rtt, 4)
3.1 状态结构体 struct bbr BBR 的私有状态比 CUBIC 复杂得多,包含带宽、RTT 估计、状态机和 ACK 聚合估计等多个子系统:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct bbr { u32 min_rtt_us; u32 min_rtt_stamp; u32 probe_rtt_done_stamp; struct minmax bw ; u32 rtt_cnt; u32 next_rtt_delivered; u64 cycle_mstamp; u32 mode:3 , prev_ca_state:3 , packet_conservation:1 , round_start:1 , ... u32 pacing_gain:10 , cwnd_gain:10 , full_bw_reached:1 , full_bw_cnt:2 , ... u32 prior_cwnd; u32 full_bw; u64 ack_epoch_mstamp; u16 extra_acked[2 ]; ... };
BBR 状态编码为 3 位位域,节省空间并对齐到 cache line。
3.2 四个运行状态 BBR 状态机由源码注释描述的状态转移图驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | V +---> STARTUP ----+ | | | | V | | DRAIN ----+ | | | | V | +---> PROBE_BW ----+ | ^ | | | | | | | +----+ | | | +---- PROBE_RTT <--+
STARTUP(启动) :以 high_gain = 2/ln(2) ≈ 2.885 的 pacing gain 指数增长,快速填满管道。每个 round 检测是否仍有 25% 以上的带宽增长,连续 3 个 round 不再增长则认为管道已满。
DRAIN(排水) :用 drain_gain = 1/high_gain ≈ 0.347 慢速发送,排空 STARTUP 阶段在队列中堆积的数据包,通常一个 RTT 内完成。
PROBE_BW(带宽探测) :稳态,8 个阶段循环(CYCLE_LEN = 8):一个 RTT 用 1.25x gain 探测带宽,一个 RTT 用 0.75x drain,其余六个 RTT 用 1.0x 巡航。pacing gain 数组:
1 2 3 4 5 6 static const int bbr_pacing_gain[] = { BBR_UNIT * 5 / 4 , BBR_UNIT * 3 / 4 , BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT };
PROBE_RTT(RTT 探测) :当 10 秒内未更新到更小的 RTT 时进入,将 cwnd 压缩到 bbr_cwnd_min_target = 4 个包,持续至少 200ms,让网络队列充分排空,重新测量真实 RTprop。代价约为 2%(200ms / 10s)。
3.3 bbr_main:主控函数 bbr_main 实现了 cong_control 接口(而非 cong_avoid),在每次 ACK 处理结束后被调用:
1 2 3 4 5 6 7 8 9 10 11 __bpf_kfunc static void bbr_main (struct sock *sk, const struct rate_sample *rs) { struct bbr *bbr = inet_csk_ca(sk); u32 bw; bbr_update_model(sk, rs); bw = bbr_bw(sk); bbr_set_pacing_rate(sk, bw, bbr->pacing_gain); bbr_set_cwnd(sk, rs, rs->acked_sacked, bw, bbr->cwnd_gain); }
bbr_update_model 是模型更新的聚合入口:
1 2 3 4 5 6 7 8 9 10 static void bbr_update_model (struct sock *sk, const struct rate_sample *rs) { bbr_update_bw(sk, rs); bbr_update_ack_aggregation(sk, rs); bbr_update_cycle_phase(sk, rs); bbr_check_full_bw_reached(sk, rs); bbr_check_drain(sk, rs); bbr_update_min_rtt(sk, rs); bbr_update_gains(sk); }
3.4 bbr_update_bw:带宽估计(BtlBw) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static void bbr_update_bw (struct sock *sk, const struct rate_sample *rs) { struct tcp_sock *tp = tcp_sk(sk); struct bbr *bbr = inet_csk_ca(sk); u64 bw; bbr->round_start = 0 ; if (rs->delivered < 0 || rs->interval_us <= 0 ) return ; if (!before(rs->prior_delivered, bbr->next_rtt_delivered)) { bbr->next_rtt_delivered = tp->delivered; bbr->rtt_cnt++; bbr->round_start = 1 ; bbr->packet_conservation = 0 ; } bbr_lt_bw_sampling(sk, rs); bw = div64_long((u64)rs->delivered * BW_UNIT, rs->interval_us); if (!rs->is_app_limited || bw >= bbr_max_bw(sk)) { minmax_running_max(&bbr->bw, bbr_bw_rtts, bbr->rtt_cnt, bw); } }
带宽估计基于 rate_sample(tcp_rate.c 填充),rs->delivered 是自某个时刻起已交付的包数,rs->interval_us 是对应的时间间隔。使用 10 RTT 的滑动最大值窗口(minmax_running_max)防止瞬时下降。
3.5 bbr_update_min_rtt:最小 RTT 估计(RTprop) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 static void bbr_update_min_rtt (struct sock *sk, const struct rate_sample *rs) { struct tcp_sock *tp = tcp_sk(sk); struct bbr *bbr = inet_csk_ca(sk); bool filter_expired; filter_expired = after(tcp_jiffies32, bbr->min_rtt_stamp + bbr_min_rtt_win_sec * HZ); if (rs->rtt_us >= 0 && (rs->rtt_us < bbr->min_rtt_us || (filter_expired && !rs->is_ack_delayed))) { bbr->min_rtt_us = rs->rtt_us; bbr->min_rtt_stamp = tcp_jiffies32; } if (bbr_probe_rtt_mode_ms > 0 && filter_expired && !bbr->idle_restart && bbr->mode != BBR_PROBE_RTT) { bbr->mode = BBR_PROBE_RTT; bbr_save_cwnd(sk); bbr->probe_rtt_done_stamp = 0 ; } if (bbr->mode == BBR_PROBE_RTT) { tp->app_limited = (tp->delivered + tcp_packets_in_flight(tp)) ? : 1 ; if (!bbr->probe_rtt_done_stamp && tcp_packets_in_flight(tp) <= bbr_cwnd_min_target) { bbr->probe_rtt_done_stamp = tcp_jiffies32 + msecs_to_jiffies(bbr_probe_rtt_mode_ms); ... } } }
3.6 bbr_set_cwnd:拥塞窗口设置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static void bbr_set_cwnd (struct sock *sk, const struct rate_sample *rs, u32 acked, u32 bw, int gain) { struct tcp_sock *tp = tcp_sk(sk); struct bbr *bbr = inet_csk_ca(sk); u32 cwnd = tcp_snd_cwnd(tp), target_cwnd = 0 ; if (!acked) goto done; if (bbr_set_cwnd_to_recover_or_restore(sk, rs, acked, &cwnd)) goto done; target_cwnd = bbr_bdp(sk, bw, gain); target_cwnd += bbr_ack_aggregation_cwnd(sk); target_cwnd = bbr_quantization_budget(sk, target_cwnd); if (bbr_full_bw_reached(sk)) cwnd = min(cwnd + acked, target_cwnd); else if (cwnd < target_cwnd || tp->delivered < TCP_INIT_CWND) cwnd = cwnd + acked; cwnd = max(cwnd, bbr_cwnd_min_target); done: tcp_snd_cwnd_set(tp, min(cwnd, tp->snd_cwnd_clamp)); if (bbr->mode == BBR_PROBE_RTT) tcp_snd_cwnd_set(tp, min(tcp_snd_cwnd(tp), bbr_cwnd_min_target)); }
3.7 Pacing Rate 设置 BBR 通过直接设置 sk->sk_pacing_rate 控制发送节奏,这依赖于 fq qdisc 或内核 pacing timer 的支持:
1 2 3 4 5 6 7 8 9 10 11 static void bbr_set_pacing_rate (struct sock *sk, u32 bw, int gain) { struct tcp_sock *tp = tcp_sk(sk); struct bbr *bbr = inet_csk_ca(sk); unsigned long rate = bbr_bw_to_pacing_rate(sk, bw, gain); if (unlikely(!bbr->has_seen_rtt && tp->srtt_us)) bbr_init_pacing_rate_from_rtt(sk); if (bbr_full_bw_reached(sk) || rate > sk->sk_pacing_rate) sk->sk_pacing_rate = rate; }
bbr_rate_bytes_per_sec 将带宽(packets/us 单位)转换为 bytes/sec,并应用 bbr_pacing_margin_percent = 1% 的下调余量,在提高吞吐的同时避免满线速发送引起队列积累:
1 2 3 4 5 6 7 8 9 static u64 bbr_rate_bytes_per_sec (struct sock *sk, u64 rate, int gain) { unsigned int mss = tcp_sk(sk)->mss_cache; rate *= mss; rate *= gain; rate >>= BBR_SCALE; rate *= USEC_PER_SEC / 100 * (100 - bbr_pacing_margin_percent); return rate >> BW_SCALE; }
BBR 初始化时会调用 cmpxchg(&sk->sk_pacing_status, SK_PACING_NONE, SK_PACING_NEEDED) 启用 pacing,告知 TCP 子系统此 socket 需要 pacing 支持。
3.8 BBR 的 TCP 友好性问题与 policer 检测 BBR 针对 token-bucket 流量整形器(policer)有专门的长期带宽采样机制 bbr_lt_bw_sampling。当 BBR 检测到两个连续采样区间内带宽一致但丢包率超过 20%(bbr_lt_loss_thresh = 50/256),它会认为自己被 policer 限速,将 pacing gain 设为 1.0 并使用长期平均带宽作为估计,避免因不断探测而在 policer 限速点附近产生大量丢包:
1 2 3 4 5 6 7 if ((diff * BBR_UNIT <= bbr_lt_bw_ratio * bbr->lt_bw) || (bbr_rate_bytes_per_sec(sk, diff, BBR_UNIT) <= bbr_lt_bw_diff)) { bbr->lt_bw = (bw + bbr->lt_bw) >> 1 ; bbr->lt_use_bw = 1 ; bbr->pacing_gain = BBR_UNIT; }
BBR 与基于丢包的算法(如 CUBIC)在混部时存在公平性问题:在浅缓冲场景(队列快满时就丢包),BBR 与 CUBIC 的竞争基本公平;但在深缓冲场景,CUBIC 会填满缓冲区后才感知到拥塞,而 BBR 不依赖丢包,会持续以估计带宽发送,理论上 BBR 流会占用更多带宽。这是 BBR 在生产部署中需要关注的问题,BBRv2 和 BBRv3 对此有改进。
四、快速重传与快速恢复 4.1 进入快速恢复:tcp_enter_recovery 当检测到三次重复 ACK(或 SACK 显示丢包)时,TCP 调用 tcp_enter_recovery(net/ipv4/tcp_input.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void tcp_enter_recovery (struct sock *sk, bool ece_ack) { struct tcp_sock *tp = tcp_sk(sk); int mib_idx; if (tcp_is_reno(tp)) mib_idx = LINUX_MIB_TCPRENORECOVERY; else mib_idx = LINUX_MIB_TCPSACKRECOVERY; NET_INC_STATS(sock_net(sk), mib_idx); tp->prior_ssthresh = 0 ; tcp_init_undo(tp); if (!tcp_in_cwnd_reduction(sk)) { if (!ece_ack) tp->prior_ssthresh = tcp_current_ssthresh(sk); tcp_init_cwnd_reduction(sk); } tcp_set_ca_state(sk, TCP_CA_Recovery); }
进入 Recovery 后:
保存当前 ssthresh(用于 undo 机制)
调用 ssthresh 回调计算新的慢启动阈值(CUBIC 约降低到 70%,Reno 降到 50%)
调用 tcp_retransmit_skb 重传丢失包
4.2 SACK 实现:tcp_sacktag_write_queue SACK(Selective Acknowledgment)允许接收方通知发送方哪些数据已收到,使发送方只重传真正丢失的包。tcp_sacktag_write_queue 是 SACK 处理的核心:
1 2 3 static int tcp_sacktag_write_queue (struct sock *sk, const struct sk_buff *ack_skb, u32 prior_snd_una, struct tcp_sacktag_state *state)
该函数遍历 SACK 块,对每个已 SACK 的 skb 设置 TCPCB_SACKED_ACKED 标记,同时通过 scoreboard 算法跟踪 in-flight 数据状态,支持六种包状态:S(Sacked)、R(Retransmitted)、L(Lost)等的组合。
Linux 6.4 主要使用 RACK(Recent ACKnowledgment)算法进行丢包检测,它基于发送时间(而非 SACK 计数)判断包是否丢失:如果一个包的发送时间比最新 SACK 包的发送时间早超过一个 RTT,则认为该包已丢失。RACK 通过 tcp_rack_reo_timeout 和重排序计时器实现,更准确地处理乱序场景。
4.3 快速恢复中的 cwnd 管理 快速恢复(Fast Recovery,RFC 3517)期间使用 “inflation” 技术临时增大 cwnd:
每收到一个重复 ACK,snd_cwnd 增加 1(补偿因重传离开网络的包)
收到新数据的 ACK(partial ACK)时,立即重传下一个未确认包,并缩减 cwnd
恢复完成(收到覆盖所有丢失包的 ACK)时,将 cwnd 设回 ssthresh
4.4 撤销机制:虚假拥塞的恢复 TCP 有一套 undo(撤销)机制,用于处理因路由变化、延迟 ACK 等原因触发的虚假拥塞响应(spurious retransmissions)。进入 Recovery 或 Loss 状态时,tcp_init_undo 保存当前 ssthresh;如果后续证明拥塞是虚假的(例如延迟 ACK 的 D-SACK 表明重传包实际已到达),则调用 undo_cwnd 回调恢复窗口。
CUBIC 的 undo_cwnd 直接使用 Reno 的实现:
1 2 3 4 5 __bpf_kfunc u32 tcp_reno_undo_cwnd (struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); return max(tcp_snd_cwnd(tp), tp->prior_cwnd); }
BBR 的 bbr_undo_cwnd 额外重置全管道检测状态,允许重新探测带宽:
1 2 3 4 5 6 7 8 9 __bpf_kfunc static u32 bbr_undo_cwnd (struct sock *sk) { struct bbr *bbr = inet_csk_ca(sk); bbr->full_bw = 0 ; bbr->full_bw_cnt = 0 ; bbr_reset_lt_bw_sampling(sk); return tcp_snd_cwnd(tcp_sk(sk)); }
检测虚假重传的主要手段是 DSACK(Duplicate SACK):如果接收方发送 DSACK 告知某个包被重复收到,说明该包的重传是不必要的,原始包只是延迟到达,发送方可以安全地撤销拥塞响应。
五、重传计时器(RTO) 5.1 计时器体系 TCP 的计时器通过 tcp_init_xmit_timers 初始化:
1 2 3 4 5 6 7 8 9 10 11 12 void tcp_init_xmit_timers (struct sock *sk) { inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer); hrtimer_init(&tcp_sk(sk)->pacing_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_PINNED_SOFT); tcp_sk(sk)->pacing_timer.function = tcp_pace_kick; hrtimer_init(&tcp_sk(sk)->compressed_ack_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED_SOFT); tcp_sk(sk)->compressed_ack_timer.function = tcp_compressed_ack_kick; }
tcp_write_timer 是重传相关的主计时器,通过 tcp_write_timer_handler 按事件类型分发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void tcp_write_timer_handler (struct sock *sk) { struct inet_connection_sock *icsk = inet_csk(sk); int event; if (((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN)) || !icsk->icsk_pending) return ; event = icsk->icsk_pending; switch (event) { case ICSK_TIME_REO_TIMEOUT: tcp_rack_reo_timeout(sk); break ; case ICSK_TIME_LOSS_PROBE: tcp_send_loss_probe(sk); break ; case ICSK_TIME_RETRANS: icsk->icsk_pending = 0 ; tcp_retransmit_timer(sk); break ; case ICSK_TIME_PROBE0: icsk->icsk_pending = 0 ; tcp_probe_timer(sk); break ; } }
5.2 tcp_retransmit_timer:RTO 超时处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void tcp_retransmit_timer (struct sock *sk) { __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPTIMEOUTS); if (tcp_write_timeout(sk)) goto out; if (icsk->icsk_retransmits == 0 ) { if (icsk->icsk_ca_state == TCP_CA_Recovery) { ... } else if (icsk->icsk_ca_state == TCP_CA_Loss) { ... } } tcp_enter_loss(sk); icsk->icsk_retransmits++; if (tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1 ) > 0 ) { inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, TCP_RESOURCE_PROBE_INTERVAL, TCP_RTO_MAX); goto out; } icsk->icsk_backoff++; out_reset_timer: icsk->icsk_rto = min(icsk->icsk_rto << 1 , TCP_RTO_MAX); inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, tcp_clamp_rto_to_user_timeout(sk), TCP_RTO_MAX); }
注释中引用了 Jacobson 的原始论文(SIGCOMM ‘88),并调侃了 120 秒上限:”我猜我们得用 TCP 以外的协议和火星通信”。
5.3 Jacobson RTT 估算 tcp_rtt_estimator(net/ipv4/tcp_input.c)实现了 RFC 6298 描述的 Jacobson/Karels RTT 估算,使用指数加权移动平均(EWMA):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static void tcp_rtt_estimator (struct sock *sk, long mrtt_us) { struct tcp_sock *tp = tcp_sk(sk); long m = mrtt_us; u32 srtt = tp->srtt_us; if (srtt != 0 ) { m -= (srtt >> 3 ); srtt += m; if (m < 0 ) { m = -m; m -= (tp->mdev_us >> 2 ); if (m > 0 ) m >>= 3 ; } else { m -= (tp->mdev_us >> 2 ); } tp->mdev_us += m; } else { srtt = m << 3 ; tp->mdev_us = m << 1 ; tp->rttvar_us = max(tp->mdev_us, tcp_rto_min_us(sk)); } tp->srtt_us = max(1U , srtt); }
RTO 由 tcp_set_rto 按 RFC 6298 公式计算:
1 RTO = SRTT + max(G, 4 * RTTVAR)
其中 G 为时钟粒度。Karn 算法 规定:不使用重传包的 ACK 更新 RTT 估计(因为无法确定 ACK 对应的是哪次发送),但 Linux 通过时间戳选项(TCP Timestamps,RFC 7323)可以精确区分,因此实际上会更新。
5.4 Tail Loss Probe(TLP):减少 RTO 依赖 Linux 6.4 引入了 TLP(ICSK_TIME_LOSS_PROBE),它是一种在 RTO 超时前主动探测尾部丢包的机制。当发送方在 max(2*SRTT, 10ms) 内未收到新的 ACK,且有数据包在途,就发送一个 TLP 探测包(通常是最后一个未确认包的重传)。如果 TLP 触发了 ACK,说明之前的包可能乱序到达,此时通过 RACK 等机制确认是否真正丢包,避免了不必要的完整 RTO 超时(RTO 超时代价大:cwnd 重置为 1,进入 Loss 状态)。
TLP 的优势在于:对于只有最后一两个包丢失的情况(tail loss),传统机制必须等待 RTO 才能检测,而 TLP 可以在一个 RTT 内完成检测和恢复,大幅降低了尾部延迟(tail latency)。
5.5 RTO 的指数退避与 user_timeout tcp_retransmit_timer 中的指数退避注释引用了 Jacobson 的原始建议,代码如下:
1 2 3 icsk->icsk_rto = min(icsk->icsk_rto << 1 , TCP_RTO_MAX);
TCP_RTO_MAX 为 120 秒,指数退避序列为:200ms、400ms、800ms … 最终达到 120s 上限后线性增加。tcp_retries2(默认 15)控制最大重传次数,约对应 924 秒(15+ 分钟)后放弃连接。
TCP_USER_TIMEOUT(socket 选项)允许用户设置连接放弃的超时上限,tcp_clamp_rto_to_user_timeout 在每次重置计时器时将 RTO 截断到用户超时的剩余时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static u32 tcp_clamp_rto_to_user_timeout (const struct sock *sk) { struct inet_connection_sock *icsk = inet_csk(sk); u32 elapsed, start_ts; s32 remaining; start_ts = tcp_sk(sk)->retrans_stamp; if (!icsk->icsk_user_timeout) return icsk->icsk_rto; elapsed = tcp_time_stamp(tcp_sk(sk)) - start_ts; remaining = icsk->icsk_user_timeout - elapsed; if (remaining <= 0 ) return 1 ; return min_t (u32, icsk->icsk_rto, msecs_to_jiffies(remaining)); }
六、拥塞控制诊断方法 6.1 使用 ss 查看实时状态 ss -tinfo 可以显示每条 TCP 连接的拥塞控制详细信息:
1 2 3 4 5 ss -tinfo ss -tin state established
输出示例:
1 2 3 4 5 6 State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 0 192.168.1.1:ssh 10.0.0.2:54321 cubic wscale:7,7 rto:204 rtt:1.5/0.5 ato:40 mss:1448 pmtu:1500 rcvmss:536 advmss:1448 cwnd:10 ssthresh:7 bytes_sent:12345 retrans:0/0 dsack_dups:0 reord_seen:0 rcv_rtt:1.5 rcv_space:29200 rcv_ssthresh:29200 minrtt:1.2
关键字段解释:
cwnd:当前拥塞窗口(包数)
ssthresh:慢启动阈值
rto:重传超时(ms)
rtt:SRTT/RTTVAR(ms)
retrans:重传次数(历史总数/当前 in-flight 重传)
6.2 查看和切换拥塞控制算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cat /proc/sys/net/ipv4/tcp_congestion_controlsysctl net.ipv4.tcp_available_congestion_control cat /proc/sys/net/ipv4/tcp_available_congestion_controlsysctl net.ipv4.tcp_allowed_congestion_control sysctl -w net.ipv4.tcp_congestion_control=bbr python3 -c " import socket TCP_CONGESTION = 13 s = socket.socket() s.setsockopt(socket.IPPROTO_TCP, TCP_CONGESTION, b'bbr\0') "
6.3 TCP 路径度量缓存 1 2 3 4 5 6 7 8 ip tcp_metrics show ip tcp_metrics delete 10.0.0.1 ip tcp_metrics show 8.8.8.8
6.4 使用 bpftrace 追踪拥塞事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 bpftrace -e 'kprobe:tcp_enter_loss { $sk = (struct sock *)arg0; printf("Loss: %s:%d -> %s:%d\n", ntop($sk->__sk_common.skc_rcv_saddr), $sk->__sk_common.skc_num, ntop($sk->__sk_common.skc_daddr), ntohs($sk->__sk_common.skc_dport)); }' bpftrace -e 'kprobe:tcp_enter_recovery { $sk = (struct sock *)arg0; $tp = (struct tcp_sock *)$sk; printf("Recovery: cwnd=%d ssthresh=%d\n", $tp->snd_cwnd, $tp->snd_ssthresh); }' bpftrace -e 'kprobe:tcp_set_ca_state { @[arg1] = count(); } interval:s:5 { print(@); clear(@); }' bpftrace -e 'kprobe:cubictcp_cong_avoid { $tp = (struct tcp_sock *)((struct sock *)arg0); @cwnd = hist($tp->snd_cwnd); }'
6.5 iperf3 + ss -i 实时监控 在一个终端运行 iperf3 服务端,另一个终端启动客户端并同时监控:
1 2 3 4 5 6 7 8 iperf3 -s iperf3 -c server_ip -t 30 -C bbr watch -n 1 'ss -tin dst server_ip | grep -A 2 ESTAB'
6.6 /proc/net/netstat 中的拥塞统计字段 1 2 3 4 grep -w 'TCPCongestionRecovery\|TCPFastRetrans\|TCPSackRecovery\ \|TCPRenoRecovery\|TCPSchedulerFailed\|TCPTimeouts\ \|TCPLostRetransmit\|TCPSACKDiscard' /proc/net/netstat
关键字段说明:
字段
含义
TCPFastRetrans
快速重传次数(三次重复 ACK 触发)
TCPCongestionRecovery
进入 Recovery 状态次数
TCPSACKRecovery
使用 SACK 的 Recovery 次数
TCPRenoRecovery
使用 Reno 方式的 Recovery 次数
TCPTimeouts
RTO 超时次数
TCPLostRetransmit
重传包本身丢失次数
TCPHystartTrainDetect
Hystart ACK Train 检测触发次数
TCPHystartDelayDetect
Hystart 延迟检测触发次数
1 2 3 4 5 6 7 8 9 10 awk '/^TcpExt/{split($0,k," ")} /^TcpExt/{ for(i=1;i<=NF;i++) keys[i]=k[i] } /^[0-9]/{ for(i=1;i<=NF;i++) vals[keys[i]]=$i } END { retrans = vals["TCPFastRetrans"] + vals["TCPTimeouts"] print "Total retrans:", retrans }' /proc/net/netstat
6.7 CUBIC 与 BBR 的调优建议 CUBIC 的关键调优参数 (通过 /sys/module/tcp_cubic/parameters/):
1 2 3 4 5 6 7 8 9 ls /sys/module/tcp_cubic/parameters/cat /sys/module/tcp_cubic/parameters/beta cat /sys/module/tcp_cubic/parameters/fast_convergence cat /sys/module/tcp_cubic/parameters/hystart echo 800 > /sys/module/tcp_cubic/parameters/beta
BBR 的最佳实践 :
BBR 需要 fq(Fair Queuing)qdisc 配合以获得最佳效果,否则内核会退回到基于 hrtimer 的 pacing(每个 socket 一个高精度定时器,资源开销大):
1 2 3 4 5 6 7 8 9 10 11 12 tc qdisc replace dev eth0 root fq tc qdisc show dev eth0 sysctl -w net.ipv4.tcp_congestion_control=bbr tc qdisc replace dev eth0 root fq ss -tin state established | grep pacing
内核缓冲区调优 (影响所有拥塞控制算法的效果):
1 2 3 4 5 6 7 8 9 10 11 12 sysctl -w net.core.rmem_max=134217728 sysctl -w net.core.wmem_max=134217728 sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728" sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728" sysctl net.ipv4.tcp_moderate_rcvbuf sysctl -w net.ipv4.tcp_notsent_lowat=16384
6.8 使用 perf 分析拥塞控制热路径 1 2 3 4 5 6 7 8 9 10 perf top -F 99 -e cycles:u --call-graph dwarf \ -- taskset -c 0 iperf3 -c server -P 8 -t 30 perf record -e cycles:k -g -F 1000 sleep 10 perf report --stdio --call-graph=graph,0.5 | grep -A 20 bbr_main perf stat -e 'tcp:tcp_cong_state_set' iperf3 -c server -t 10
七、CUBIC 与 BBR 对比及场景选择
维度
CUBIC
BBR
拥塞信号
丢包(reactive)
带宽 + RTT(proactive)
算法复杂度
中(三次函数 + Hystart)
高(四状态机 + 多种采样)
高 BDP 网络吞吐
好
非常好
延迟表现
中(会填满缓冲区)
好(主动控制队列长度)
bufferbloat 场景
差(填满深缓冲)
好(不依赖丢包)
无线/卫星链路
差(随机丢包误判为拥塞)
好(通过 RTT 区分拥塞与丢包)
混部公平性(与 Reno)
好
中(深缓冲场景占优)
pacing 依赖
否
是(需要 fq qdisc 最佳效果)
内核状态大小
~40 bytes(struct bictcp)
~128 bytes(struct bbr)
默认内核
是(Linux 2.6.19+)
否(需手动启用)
场景推荐 :
数据中心内部、低延迟高带宽 :BBR,配合 fq + ECN
广域网、存在 bufferbloat :BBR,在测量延迟增加前就控制发送速率
无线、卫星链路(随机丢包高) :BBR,避免把随机丢包误判为拥塞
多租户混部、严格公平性要求 :CUBIC(BBRv2 对公平性有改进)
兼容性优先 :CUBIC,作为 Linux 默认算法,经过多年大规模验证
八、源码导读路径 理解 TCP 拥塞控制的推荐阅读顺序:struct tcp_congestion_ops、struct bictcp、struct bbr 等核心数据结构定义
**net/ipv4/tcp_cong.c**:拥塞控制框架,算法注册/注销、初始化、Reno 基准实现
**net/ipv4/tcp_cubic.c**:CUBIC 完整实现(约 560 行),包含三次函数、Hystart、Fast Convergence
**net/ipv4/tcp_bbr.c**:BBR 完整实现(约 1200 行),包含四状态机、带宽估计、RTT 估计、pacing
**net/ipv4/tcp_input.c**:
tcp_rtt_estimator(约 L828):Jacobson RTT 估算
tcp_enter_recovery(约 L2801):进入快速恢复
tcp_sacktag_write_queue(约 L1809):SACK 处理
tcp_fastretrans_alert:拥塞状态机驱动
**net/ipv4/tcp_timer.c**:
tcp_retransmit_timer:RTO 超时处理与指数退避
tcp_write_timer_handler:计时器事件分发
**net/ipv4/tcp_rate.c**:rate_sample 数据结构的填充逻辑,BBR 带宽估计的基础
总结 Linux TCP 拥塞控制框架通过操作集接口实现了优雅的可插拔设计,内核维护着算法链表并通过 RCU 保证读侧无锁访问。CUBIC 以三次函数逼近理想的公平带宽增长曲线,在高 BDP 网络中远优于 Reno,其 Hystart 机制解决了慢启动过度的问题。BBR 代表了拥塞控制的范式转移——从被动响应丢包信号转向主动建模网络路径,通过精确估计 BtlBw 和 RTprop 实现高吞吐低延迟,在存在浅缓冲和 bufferbloat 的场景下尤为出色。
在实际系统中,选择拥塞控制算法需结合网络特征(带宽、延迟、丢包率)、业务特点(长连接/短连接、延迟敏感度)和基础设施(是否部署 fq qdisc 支持 pacing)综合权衡。通过 ss、bpftrace、/proc/net/netstat 等工具,可以在生产环境中持续观测拥塞控制行为,为调优提供数据支撑。