Linux 网络内核协议栈深度剖析(五):TCP 拥塞控制算法源码解析

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 {
/* fast path fields are put first to fill one cache line */

/* return slow start threshold (required) */
u32 (*ssthresh)(struct sock *sk);

/* do new cwnd calculation (required) */
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);

/* call before changing ca_state (optional) */
void (*set_state)(struct sock *sk, u8 new_state);

/* call when cwnd event occurs (optional) */
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);

/* call when ack arrives (optional) */
void (*in_ack_event)(struct sock *sk, u32 flags);

/* hook for packet ack accounting (optional) */
void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);

/* override sysctl_tcp_min_tso_segs */
u32 (*min_tso_segs)(struct sock *sk);

/* call when packets are delivered to update cwnd and pacing rate,
* after all the ca_state processing. (optional)
*/
void (*cong_control)(struct sock *sk, const struct rate_sample *rs);

/* new value of cwnd after loss (required) */
u32 (*undo_cwnd)(struct sock *sk);

/* get info for inet_diag (optional) */
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;

/* initialize private data (optional) */
void (*init)(struct sock *sk);
/* cleanup private data (optional) */
void (*release)(struct sock *sk);
} ____cacheline_aligned_in_smp;

结构体中有三个回调是必须实现的(required):

  • ssthresh:丢包后计算新的慢启动阈值
  • cong_avoidcong_control:实现拥塞避免逻辑(二选一)
  • undo_cwnd:虚假拥塞撤销时恢复窗口

tcp_validate_congestion_control 在注册时强制校验这三个字段:

1
2
3
4
5
6
7
8
9
10
11
// net/ipv4/tcp_cong.c
int tcp_validate_congestion_control(struct tcp_congestion_ops *ca)
{
/* all algorithms must implement these */
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_startnet/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; /* 返回剩余未处理的 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 credits accumulated at a higher w, apply them gently now. */
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; /* 区间起点时的 tp->delivered */
s32 delivered; /* 区间内已确认的包数 */
long interval_us; /* 区间时长(us) */
long rtt_us; /* 最新 (S)ACKed 包的 RTT,-1 表示无效 */
int losses; /* 本次 ACK 处理中新标记为丢失的包数 */
u32 acked_sacked; /* 本次 ACK 新确认/SACK 的包数 */
u32 prior_in_flight; /* ACK 前的 in-flight 包数 */
bool is_app_limited; /* 样本是否来自应用受限期间 */
bool is_retrans; /* 是否为重传包的 ACK */
bool is_ack_delayed; /* 是否为延迟 ACK */
};

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; /* increase cwnd by 1 after ACKs */
u32 last_max_cwnd; /* last maximum snd_cwnd */
u32 last_cwnd; /* the last snd_cwnd */
u32 last_time; /* time when updated last_cwnd */
u32 bic_origin_point; /* origin point of bic function */
u32 bic_K; /* time to origin point from epoch start */
u32 delay_min; /* min delay (usec) */
u32 epoch_start; /* beginning of an epoch */
u32 ack_cnt; /* number of acks */
u32 tcp_cwnd; /* estimated tcp cwnd */
u32 round_start; /* beginning of each round */
u32 end_seq; /* end_seq of the round */
u32 last_ack; /* last time when the ACK spacing is close */
u32 curr_rtt; /* the minimum rtt of current round */
};

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 {
/* Compute new K based on
* (wmax-cwnd) * (srtt>>3 / HZ) / c * 2^(3*bictcp_HZ)
*/
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[] = {
/* 预计算的 cbrt 查找表 */
0, 54, 54, 54, 118, 118, ...
};
b = fls64(a);
/* ... 查表得到初始估计值 */
/* Newton-Raphson: x_{k+1} = (2*x_k + a/x_k^2) / 3 */
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; /* 累计 ACK 的包数 */

/* 同一 jiffy 内只计算一次,避免重复计算 */
if (ca->last_cwnd == cwnd &&
(s32)(tcp_jiffies32 - ca->last_time) <= HZ / 32)
return;

ca->last_cwnd = cwnd;
ca->last_time = tcp_jiffies32;

/* 计算从 epoch_start 到现在的时间 t,单位转换为 bictcp_HZ (2^10) */
t = (s32)(tcp_jiffies32 - ca->epoch_start);
t += usecs_to_jiffies(ca->delay_min);
t <<= BICTCP_HZ; /* 单位: bictcp_HZ ticks */
do_div(t, HZ);

/* offs = |t - K|,即距离原点的时间偏差 */
if (t < ca->bic_K)
offs = ca->bic_K - t;
else
offs = t - ca->bic_K;

/* delta = C/rtt * (t-K)^3,对应三次函数的增量 */
delta = (cube_rtt_scale * offs * offs * offs) >> (10 + 3*BICTCP_HZ);

/* 根据 t 与 K 的关系确定目标窗口方向 */
if (t < ca->bic_K) /* 还未到达 W_max,继续恢复 */
bic_target = ca->bic_origin_point - delta;
else /* 已超过 W_max,继续探测 */
bic_target = ca->bic_origin_point + delta;

/* 计算 cnt:每 cnt 个 ACK 增加一个 cwnd */
if (bic_target > cwnd) {
ca->cnt = cwnd / (bic_target - cwnd);
} else {
ca->cnt = 100 * cwnd; /* 目标已到达,极小增量 */
}

tcp_friendliness:
/* TCP 友好性:保证不慢于标准 Reno */
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;
}
}

/* 最大增速:每 2 个 ACK 最多增加 1,即 1.5x/RTT */
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; /* 结束当前 epoch */

/* Fast Convergence:若当前窗口小于历史最大值,
* 说明新流竞争使网络容量收缩,主动让出更多带宽 */
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);

/* ssthresh = cwnd * beta / 1024,beta 默认 717,即约 0.7 */
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;

/* 仅在慢启动且 cwnd 大于 hystart_low_window(16) 时触发 */
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.ccubictcp_register):

1
2
3
4
5
6
7
8
9
10
11
12
13
/* beta_scale = 8 * (1024 + beta) / 3 / (1024 - beta)
* 用于 tcp_friendliness 部分的 Reno 等效增长率计算 */
beta_scale = 8*(BICTCP_BETA_SCALE+beta) / 3
/ (BICTCP_BETA_SCALE - beta);

/* cube_rtt_scale = bic_scale * 10 = 410
* 对应 C/rtt,其中 C = bic_scale/1024 ≈ 0.04,rtt = 100ms */
cube_rtt_scale = (bic_scale * 10);

/* cube_factor = 2^40 / (bic_scale * 10)
* 预编码 1/C/rtt 因子,用于计算 K = cubic_root(cube_factor * Δwnd) */
cube_factor = 1ull << (10+3*BICTCP_HZ); /* 2^40 */
do_div(cube_factor, bic_scale * 10);

这里的关键设计思想是将 SRTT 固定为 100ms 作为归一化基准来计算 cube_factor,将时间相关的缩放因子从运行时热路径移到模块初始化阶段,使 bictcp_update 中的计算纯粹是整数乘除操作,对 CPU 缓存友好。

CUBIC 的 cubictcp_cong_avoidcong_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; /* 10 秒窗口内的最小 RTT */
u32 min_rtt_stamp; /* min_rtt_us 的时间戳 */
u32 probe_rtt_done_stamp; /* PROBE_RTT 模式结束时间 */
struct minmax bw; /* 带宽最大值滑动窗口(10 RTT) */
u32 rtt_cnt; /* packet-timed round 计数 */
u32 next_rtt_delivered; /* 下一个 round 的起点 */
u64 cycle_mstamp; /* 当前 cycle phase 的起始时间 */
u32 mode:3, /* 当前 BBR 状态(bbr_mode) */
prev_ca_state:3, /* 上一个 ACK 时的 CA 状态 */
packet_conservation:1, /* 是否使用 packet conservation */
round_start:1, /* 当前 ACK 是否开始新的 round */
...
u32 pacing_gain:10, /* pacing rate 增益 */
cwnd_gain:10, /* cwnd 增益 */
full_bw_reached:1, /* STARTUP 是否已填满管道 */
full_bw_cnt:2, /* 未见显著 bw 增长的 round 数 */
...
u32 prior_cwnd; /* 进入 loss recovery 前的 cwnd */
u32 full_bw; /* 用于检测管道是否填满 */
/* ACK 聚合估计 */
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, /* 以 1.0 * bw 巡航 */
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); /* 更新带宽和 RTT 模型 */

bw = bbr_bw(sk);
bbr_set_pacing_rate(sk, bw, bbr->pacing_gain); /* 设置 pacing rate */
bbr_set_cwnd(sk, rs, rs->acked_sacked, bw, bbr->cwnd_gain); /* 设置 cwnd */
}

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); /* 更新 ACK 聚合估计 */
bbr_update_cycle_phase(sk, rs); /* PROBE_BW cycle 推进 */
bbr_check_full_bw_reached(sk, rs); /* 检测管道是否已满 */
bbr_check_drain(sk, rs); /* 检查是否应退出 DRAIN */
bbr_update_min_rtt(sk, rs); /* 更新最小 RTT */
bbr_update_gains(sk); /* 根据状态更新 gain */
}

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; /* 无效观测,跳过 */

/* 检测是否进入新的 RTT round */
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); /* 长期带宽采样(policer 检测)*/

/* 带宽样本 = delivered / interval_us(单位:packets/us,左移 24 位) */
bw = div64_long((u64)rs->delivered * BW_UNIT, rs->interval_us);

/* app-limited 样本只在不低于当前估计时才采纳 */
if (!rs->is_app_limited || bw >= bbr_max_bw(sk)) {
/* 10 个 RTT 的滑动最大值窗口 */
minmax_running_max(&bbr->bw, bbr_bw_rtts, bbr->rtt_cnt, bw);
}
}

带宽估计基于 rate_sampletcp_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;

/* 检查 10 秒窗口是否过期 */
filter_expired = after(tcp_jiffies32,
bbr->min_rtt_stamp + bbr_min_rtt_win_sec * HZ);

/* 更新 min_rtt:新的 RTT 样本更小,或窗口过期且 ACK 非延迟 */
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;
}

/* 窗口过期且不在 PROBE_RTT:触发 RTT 探测 */
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) {
/* 将 in-flight 压缩到 4 包以下,测量真实 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 = BDP = bw * min_rtt,乘以 cwnd_gain */
target_cwnd = bbr_bdp(sk, bw, gain);

/* 叠加 ACK 聚合估计的额外空间 */
target_cwnd += bbr_ack_aggregation_cwnd(sk);
target_cwnd = bbr_quantization_budget(sk, target_cwnd);

/* 慢速爬升到目标 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
/* 两次区间的带宽差异 <= 1/8,且绝对差 <= 4Kbps,认为被 policed */
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_recoverynet/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); /* 保存 undo 上下文 */

if (!tcp_in_cwnd_reduction(sk)) {
if (!ece_ack)
tp->prior_ssthresh = tcp_current_ssthresh(sk);
tcp_init_cwnd_reduction(sk); /* 启动 cwnd 缩减 */
}
tcp_set_ca_state(sk, TCP_CA_Recovery); /* 转换到 Recovery 状态 */
}

进入 Recovery 后:

  1. 保存当前 ssthresh(用于 undo 机制)
  2. 调用 ssthresh 回调计算新的慢启动阈值(CUBIC 约降低到 70%,Reno 降到 50%)
  3. 调用 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); /* RACK 重排序超时 */
break;
case ICSK_TIME_LOSS_PROBE:
tcp_send_loss_probe(sk); /* TLP 尾部丢包探测 */
break;
case ICSK_TIME_RETRANS:
icsk->icsk_pending = 0;
tcp_retransmit_timer(sk); /* RTO 超时重传 */
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)
{
/* ... FastOpen 特殊处理 ... */

__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPTIMEOUTS);
if (tcp_write_timeout(sk))
goto out;

/* 记录丢包统计 MIB */
if (icsk->icsk_retransmits == 0) {
/* 根据 CA 状态选择对应 MIB 计数器 */
if (icsk->icsk_ca_state == TCP_CA_Recovery) { ... }
else if (icsk->icsk_ca_state == TCP_CA_Loss) { ... }
}

tcp_enter_loss(sk); /* 进入 Loss 状态,调用 ssthresh 回调 */

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;
}

/* Jacobson 指数退避:RTO = RTO * 2,上限 TCP_RTO_MAX (120s) */
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_estimatornet/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; /* 新 RTT 测量值 */
u32 srtt = tp->srtt_us;

if (srtt != 0) {
m -= (srtt >> 3); /* err = m - srtt/8 */
srtt += m; /* srtt = 7/8 srtt + 1/8 new(α = 1/8)*/
if (m < 0) {
m = -m;
m -= (tp->mdev_us >> 2);
if (m > 0) m >>= 3; /* 误差减小时 mdev 收敛更慢 */
} else {
m -= (tp->mdev_us >> 2);
}
tp->mdev_us += m; /* mdev = 3/4 mdev + 1/4 |err|(β = 1/4)*/
/* 更新 rttvar... */
} else {
/* 第一次测量:srtt = m,mdev = 2m,rttvar = max(mdev, rto_min) */
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
/* Jacobson (SIGCOMM 88) suggests doubling rto each time is
* the least we can get away with. */
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
# 查看所有 TCP 连接的拥塞窗口和 RTT
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_control

# 查看所有可用算法
sysctl net.ipv4.tcp_available_congestion_control
# 或
cat /proc/sys/net/ipv4/tcp_available_congestion_control

# 查看允许非特权用户使用的算法
sysctl net.ipv4.tcp_allowed_congestion_control

# 切换到 BBR
sysctl -w net.ipv4.tcp_congestion_control=bbr

# 单个 socket 级别切换(需要 CAP_NET_ADMIN 或算法被标记为 non-restricted)
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
# 查看 TCP 路径度量缓存(包含历史 RTT、cwnd 等)
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
# 追踪 RTO 超时进入 Loss 状态的事件
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));
}'

# 追踪快速重传触发(进入 Recovery 状态)
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(@); }'

# 追踪 cwnd 变化(需 CUBIC)
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
# 终端 1:服务端
iperf3 -s

# 终端 2:客户端,使用 BBR
iperf3 -c server_ip -t 30 -C bbr

# 终端 3:实时监控 TCP 状态(每秒刷新)
watch -n 1 'ss -tin dst server_ip | grep -A 2 ESTAB'

6.6 /proc/net/netstat 中的拥塞统计字段

1
2
3
4
# 解析 netstat 中的拥塞控制相关计数器
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
# 查看 CUBIC 当前参数
ls /sys/module/tcp_cubic/parameters/
cat /sys/module/tcp_cubic/parameters/beta # 默认 717(≈0.7)
cat /sys/module/tcp_cubic/parameters/fast_convergence # 默认 1(开启)
cat /sys/module/tcp_cubic/parameters/hystart # 默认 1(开启)

# 对于高速低延迟内网(如 25GbE)可适当增大 beta 以减少丢包后的退缩
# 警告:生产环境修改需谨慎测试
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
# 为 eth0 启用 fq qdisc(生产环境推荐)
tc qdisc replace dev eth0 root fq

# 检查当前 qdisc
tc qdisc show dev eth0

# 启用 BBR 并配合 fq
sysctl -w net.ipv4.tcp_congestion_control=bbr
tc qdisc replace dev eth0 root fq

# 验证 pacing 状态(SK_PACING_NEEDED = 1)
ss -tin state established | grep pacing

内核缓冲区调优(影响所有拥塞控制算法的效果):

1
2
3
4
5
6
7
8
9
10
11
12
# 增大 TCP 缓冲区上限(对于 10Gbps+ 网络必要)
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"

# 启用 TCP 自动调优(默认已启用)
sysctl net.ipv4.tcp_moderate_rcvbuf # 应为 1

# BBR 专用:增大发送缓冲区以支持更大的 BDP
# BBR 的 bbr_sndbuf_expand 返回 3(3 * cwnd),需要更大的发送缓冲
sysctl -w net.ipv4.tcp_notsent_lowat=16384

6.8 使用 perf 分析拥塞控制热路径

1
2
3
4
5
6
7
8
9
10
# 分析 tcp_cong_avoid_ai 和 bictcp_update 的 CPU 消耗
perf top -F 99 -e cycles:u --call-graph dwarf \
-- taskset -c 0 iperf3 -c server -P 8 -t 30

# 记录 BBR 主函数调用栈
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_opsstruct bictcpstruct bbr 等核心数据结构定义

  1. **net/ipv4/tcp_cong.c**:拥塞控制框架,算法注册/注销、初始化、Reno 基准实现

  2. **net/ipv4/tcp_cubic.c**:CUBIC 完整实现(约 560 行),包含三次函数、Hystart、Fast Convergence

  3. **net/ipv4/tcp_bbr.c**:BBR 完整实现(约 1200 行),包含四状态机、带宽估计、RTT 估计、pacing

  4. **net/ipv4/tcp_input.c**:

    • tcp_rtt_estimator(约 L828):Jacobson RTT 估算
    • tcp_enter_recovery(约 L2801):进入快速恢复
    • tcp_sacktag_write_queue(约 L1809):SACK 处理
    • tcp_fastretrans_alert:拥塞状态机驱动
  5. **net/ipv4/tcp_timer.c**:

    • tcp_retransmit_timer:RTO 超时处理与指数退避
    • tcp_write_timer_handler:计时器事件分发
  6. **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 等工具,可以在生产环境中持续观测拥塞控制行为,为调优提供数据支撑。