KCC 重传率异常的定位与修复:基于内核 DUMP + 数学公式 + AI 双向推理的拥塞控制调试方法
KCC 重传率异常的定位与修复:基于内核 DUMP + 数学公式 + AI 双向推理的拥塞控制调试方法
KCC 在 1Gbps 数据中心链路上的 10 秒重传计数稳定在 38 万,内核 BBR 只有 9.5 万。吞吐量相同(1.10 Gbps),重传多 4 倍。
问题在 BBR 状态机的实现里,不在 Kalman/ECN 等 KCC 扩展。最终根因是 kcc_update_bw 中的 LT BW auto-recovery 代码——一个用峰值带宽比较平均带宽的统计错误,在拥塞控制的反馈环里被放大了约 400 倍。
下面记录完整的排查过程、方法论和修复。
1. 方法论:拥塞控制算法的调试与其他系统软件有本质区别
传统软件调试的思路是:加断点、看变量、单步跟踪、修 bug、重跑。这对拥塞控制算法不适用。
拥塞控制算法是一个闭环控制系统。它根据 ACK 反馈的 delivery rate 和 RTT 调整发送窗口和 pacing rate,而这些调整又反过来影响后续的 delivery rate 和 RTT。任何一个环节的偏差都会在反馈环里被反复放大。你不能"停在断点看变量",因为一旦停下来,整个控制回路就断开了。
在这个项目里,我们使用的方法是:
1.1 内核 DUMP + 采样打印
在所有关键决策点(kcc_set_cwnd、kcc_update_bw、kcc_is_next_cycle_phase)插入和内核 BBR 格式完全一致的 pr_warn 输出:
pr_warn("KCC target: bdp=%u q_add=%u agg_add=%u target=%u "
"mode=%u gain=%u bw=%u mrtt=%u\n",
raw_bdp, q_add, agg_add, target_cwnd,
kcc->mode, gain, bw, kcc->min_rtt_us);
然后用 BBR 跑同样的链路,用完全相同的格式输出 BBR 的采样:
BBR target: bdp=59 q_add=275 agg_add=18 target=352 mode=3 gain=320 bw=1358954 mrtt=364
KCC target: bdp=86 q_add=278 agg_add=20 target=384 mode=3 gain=320 bw=1918614 mrtt=372
不是看"哪个变量不对",而是看整条计算链:从 bw 开始,经过 bdp(bw, min_rtt, gain),再加上 ack_agg 和 quantization_budget,最终到 target_cwnd。如果 bw 已经偏了,后面的 bdp、target 必然全偏——不用去排查后段。
1.2 数学公式 + AI 双向推理
找到偏差点后,不盲目改代码。先根据 BBR 的数学公式手工推算理论值,再和 DUMP 出来的实际值交叉验证。
例如,BBR 的 BDP 公式:
bdp = ceil( (bw * min_rtt * gain >> BBR_SCALE) / BW_UNIT )
其中 BW_SCALE = 24,BBR_SCALE = 8,BW_UNIT = 1 << 24 = 16777216,BBR_UNIT = 256。
对于 bw = 1358954,min_rtt = 364,gain = 320(1.25x):
w = 1358954 * 364 = 494,660,056
w_gain = 494,660,056 * 320 = 158,291,217,920
>> 8 = 618,324,288
bdp = (618,324,288 + 16777216 - 1) / 16777216 = 37 段
bdp_raw (without cwnd_gain) ≈ 59 段(带 2x cwnd_gain 的完整 BDP)
KCC 的 bw = 1918614,同样的公式:
w = 1918614 * 372 = 713,724,408
w_gain = 713,724,408 * 320 = 228,391,810,560
>> 8 = 891,374,260
bdp = (891,374,260 + 16777216 - 1) / 16777216 = 53 段
→ 带 2x cwnd_gain ≈ 86 段
所以问题不在 BDP 公式本身(公式是一样的),而在为什么 KCC 的 bw 比 BBR 高 41%。
我们用 AI 辅助计算了各种可能:div_u64 vs div64_long 的整除差异、mss 乘法溢出的截断偏差、margin 百分比的整数舍弃——全部排除。每一项差异的理论影响都在 0.01% 量级,不可能解释 41%。
然后开始反向推理:如果 bw 比 BBR 高 41%,而所有 bw 的计算路径一致,那么 KCC 接收到的 delivery rate 采样本身就必须更高。这意味着发送端在相同的时间内实际发出了更多的数据。为什么会发出更多数据?因为 CWND 更大。为什么 CWND 更大?因为 BDP 更大。为什么 BDP 更大?因为 max_bw 更大。
这是一个闭环推论:max_bw 偏高 → CWND 偏大 → inflight 偏高 → delivery rate 采样偏高 → max_bw 维持偏高。我们需要找到是什么启动了这个正反馈循环。
1.3 逐特性回退验证
我们做了第四步:从 BBRv1 补丁版(已验证 118k 丢包)出发,纯机械改名 bbr→kcc,注册为 kcc 模块。这个基座跑出来丢包率 126k——和 BBRv1 一致。说明 KCC 的扩展(Kalman、ECN 等)本身没有引入问题。
然后逐个往回叠加 KCC 特性,每加一个测一次 iperf3:
| 特性 | 10s 丢包 | 结论 |
|---|---|---|
| BBRv1 基座(纯改名) | 126k | 基准 |
| + Kalman 过滤器 | 126k | 无影响 |
| + ECN/置信度/单流检测 | 126k | 无影响 |
| + PROBE_RTT 解耦 | 126k | 无影响 |
| + LT BW auto-recovery | 320k | 元凶 |
这就是传统调试做不到的地方——你不能"改一行代码看效果"然后下一个断点继续。你必须整体回退、整体重构、逐个叠加、逐次验证。每一次测试都是完整的 10 秒 iperf3 运行,需要远端服务器配合。整个循环跑了约 40 次测试、累计约 8 小时。
2. 根因分析
2.1 BBR 的 LT BW(Long-Term Bandwidth)
BBR 的 LT BW 机制用于检测 token-bucket 流量整形器(policer):
- 丢包发生时,开始 LT BW 采样(
lt_is_sampling = true) - 收集
bbr_lt_intvl_min_rtts(4 个 RTT)的带宽数据 - 在下一个丢包时计算区间的 delivery rate
- 如果两个连续区间的带宽一致(差值 ≤ 1/8 或 ≤ 4 Kbps),且丢包率 ≥ 20%(
bbr_lt_loss_thresh = 50,即 50/256 ≈ 19.5%),则判定为 policer - 激活
lt_use_bw = 1,pacing_gain 锁为BBR_UNIT(1.0x),cwnd_gain 不变(2x) - 每 RTT 递增
lt_rtt_cnt,达到bbr_lt_bw_max_rtts = 48后自动退出
关键点:BBR 的 LT BW 退出是被动计时,不做信号判断。
2.2 KCC 加了一层主动判断
KCC 在 kcc_update_bw 末尾添加了 LT BW auto-recovery:
if (unlikely(kcc->lt_use_bw)) {
if ((u64)cur_max * ratio_den > (u64)kcc->lt_bw * ratio_num) {
kcc->lt_restore_cnt++;
if (kcc->lt_restore_cnt >= consec_acks) {
kcc_reset_lt_bw_sampling(sk);
kcc_reset_mode(sk);
}
} else {
kcc->lt_restore_cnt = 0;
}
}
参数:ratio_num/ratio_den = 5/4 = 1.25x,consec_acks = 3。
逻辑:如果 max_bw > lt_bw × 1.25 持续 3 个 ACK,就认为路径恢复了,退出 LT BW。
2.3 为什么这是统计错误
kcc_max_bw(sk) 返回的是 struct minmax 滑动窗口(bbr_bw_rtts = CYCLE_LEN + 2 = 10 个 RTT)内的最大带宽样本。而 kcc->lt_bw 是 BBR 对两段丢包区间之间带宽的均值估计。
在 1Gbps 数据中心链路上,窗口最大带宽的分布是这样的:如果平均带宽是 135 万 BW_UNIT,10 个 RTT 窗口内的最大值通常在 150-200 万(取决于瞬时队列波动)。而 LT 均值跟踪的是丢包区间内的平均速率,远低于窗口峰值。
所以 max_bw > lt_bw × 1.25 在正常的统计涨落下几乎是恒等式。这不是 bug 的产物,这是统计的必然。
max_bw > avg(lt_bw)在正常随机过程中有超过 99% 的概率成立。用 1.25x 乘子去"判断路径恢复"等价于没有判断——它永远触发。
2.4 振荡的正反馈机制
一旦 auto-recovery 触发,kcc_reset_lt_bw_sampling(sk) 清零 lt_bw、lt_use_bw、lt_is_sampling。Pacing_gain 恢复到正常的 cycle gain(可能是 1.25x probe 或 0.75x drain 或 1.0x cruise)。
但此时 BBR 的丢包信号可能仍然存在(因为之前 lt_use_bw 切换导致 pacing 波动引入更多丢包),于是 BBR 重新触发 LT BW 采样 → 重新激活 lt_use_bw → KCC 再次检测到 max_bw > lt_bw × 1.25 → 再次重置。循环往复。
这个振荡的后果:
pacing_gain在BBR_UNIT(1.0x)和 cycle gain(1.25x/0.75x/1.0x)之间高频切换- 切换瞬间产生异常的 delivery rate 采样
- minmax 滑动窗口(非对称的 max 滤波器)捕获异常峰值,永不下落
max_bw被持久推高约 41%,稳定在一个新的、虚假的均衡态- 这个虚假的
max_bw进入 BDP 计算 → CWND 膨胀 → inflight 膨胀 → 丢包增加 - 更多的丢包又加剧了 LT BW 的激活频率 → 循环强度增加
这是一个经典的二阶反馈不稳定。 控制理论中,如果在反馈信号上叠加一个错误的检测器,且检测器本身的输出与它检测的对象正相关,就会产生振荡发散。
2.5 数学验证
振荡周期近似为 LT BW 激活后的最短复位时间:
T_osc ≈ 3 ACK × RTT
≈ 3 × 0.37 ms ≈ 1.1 ms (数据中心)
≈ 3 × 5 ms ≈ 15 ms (边缘节点)
这个频率远高于 BBR 的 8 相位增益循环周期(8 × RTT ≈ 3 ms),所以 pacing_gain 在单个增益相位内会被多次翻转。
假设无振荡时的稳态 max_bw ≈ 136 万:
实际 max_bw = 136 万 × f(振荡幅度)
f(振荡) ≈ 1 + (probe_gain - 1) × T_up / (T_up + T_down + T_stable)
≈ 1 + 0.25 × 0.3 = 1.075 (如果 up 时间占 30%)
但实际观测到的是 1.41 倍的偏差(191 万 vs 136 万),这意味着 T_up(高 pacing 的时间占比)远高于 30%。因为在 lt_use_bw 被 auto-recovery 清除后,KCC 的 pacing_gain 在无 LT 保护下直接进入下一次 gain cycle 的 probe up 阶段(1.25x)。这个 probe up 阶段在没有 LT 保护的情况下会产生超额 inflight,制造更多丢包,更早触发下一次 LT 激活,形成一个"加速"的正反馈。
公式推演:设无振荡时
max_bw = B0。每次 auto-recovery 触发后,KCC 进入约 1/8 周期的 1.25x probe up(平均 pacing = 1.25,而无 LT 保护的 BBR 平均 pacing = 1.0)。设 probe 期间 delivery rate 约等于 pacing rate,则单次 probe up 产生的额外 delivery:ΔB ≈ B0 × (1.25 - 1.0) × (T_probe / T_cycle) = B0 × 0.25 × (1/8) = B0 × 0.03125设振荡频率为每 RTT 1-2 次翻转,10 个 RTT 窗口内约 15 次翻转,每次翻转注入
ΔB。由于 minmax 是 max 滤波器,它会保留这些尖峰。在 10 RTT 的窗口内,15 次翻转意味着至少 15 次 probe up 注入。minmax 保留最大值,且不随时间衰减(不同与 EWMA),所以窗口内的 max 会随着翻转次数逐步攀升:max_bw ≈ B0 + Σ ΔB_i (窗口内的累积最大值) ≈ B0 + 15 × 0.03125 × B0 ≈ B0 × 1.47这与观测到的 1.41 倍在理论误差范围内。
3. 修复
唯一修改:删除 kcc_update_bw 中的 LT BW auto-recovery 代码块(约 30 行,含参数读取和计数器操作)。
BBR 自带的 48-RTT 超时恢复完全够用。LT BW 的设计意图就是在 policer 存在时保守发送,等足够长时间后再恢复。主动判断"路径是否恢复了"是一个错误的目标——在统计涨落下不可能有一个可靠的、单变量的在线判据。
4. 附带修复
在排查过程中发现的其他问题和修复(按发现顺序):
-
PROBE_RTT 退出后 2.89x 高增益爆发:KCC 在 PROBE_RTT 退出后设置
pacing_gain = kcc_high_gain_val(2.89x)。内核 BBR 退出 PROBE_RTT 直接走正常的 cycle gain。这个单 ACK 的爆发注入异常高的 delivery rate 采样,放大max_bw。修复:删除,改走正常 cycle gain。 -
LT BW 激活后的 probe 放大:KCC 在
lt_use_bw激活时用kcc_get_cycle_pacing_gain()获取 cycle gain 再叠加 +10%~20%。内核 BBR 在lt_use_bw时锁 pacing 在BBR_UNIT(1.0x)。KCC 的放大行为直接违反 LT BW 的设计目的。修复:锁 pacing 为BBR_UNIT,匹配内核 BBR。 -
tcp_stamp_us_delta 返回值截断:
u32 raw = tcp_stamp_us_delta(...)将 s64 返回值截断为 u32。tcp_stamp_us_delta(t1, t2) = (s64)(t1 - t2),当t1 < t2时结果为负值,截断为 u32 后变成约 40 亿的巨值,导致is_full_length恒为 true——增益相位在一个 ACK 内就完成切换,产生微突发。修复:u32 raw→s64 delta。 -
s64/u64 混合比较:
(u64)kcc->min_rtt_us与 s64 delta 比较,有符号和无符号的混合在 delta 为负时产生错误结果。修复:去掉所有(u64)强转。 -
cycle_idx 掩码:
kcc->cycle_idx == 0在 8 位 bitfield + 8 周期长度下工作正常,但当cycle_len被配置为其他值(如 256)时,probe bonus 被注入到了所有非 0 的相位。修复:kcc->cycle_idx & (kcc_probe_bw_cycle_len_val - 1) == 0。 -
ceil→floor:所有参数推导(
high_gain、drain_gain、cwnd_gain、full_bw_thresh)从 ceiling 除法改为标准截断除法,和内核 BBR 的定点整数运算行为一致。 -
类型对齐:函数签名
u32 gain→int gain、u64 bw→u32 bw,匹配内核 BBR v5.4 的函数原型。 -
kcc_set_state 对齐:删除冗余的
full_bw_cnt = 0、packet_conservation = 0,lt_bw_sampling调用去条件判断,一字不差匹配内核 BBR。 -
min_rtt 比较
<→<=:内核 BBR v5.4 用<=,KCC 用了<。<=使相等的 RTT 也刷新 min_rtt 时间戳,减少不必要的 PROBE_RTT 触发。 -
死代码删除:
KCC_LT_BW_PROBE_RAMP_RTTS、kcc_lt_bw_probe_pct、KCC_LT_RESTORE_CNT_MAX、kcc_lt_restore_ratio_num/den、kcc_lt_restore_consec_acks全链路删除。
5. 结果
| 版本 | 10s 重传 | 吞吐量 | 改善 |
|---|---|---|---|
| KCC 原版 | 380,000 | 1.10 Gbps | — |
| KCC 修复后 | 135,000 | 1.10 Gbps | ↓ 64% |
| 内核 BBR v5.4 | 95,000 | 1.08 Gbps | 参考基准 |
剩余约 40k 的差距来自 KCC 的功能性开销:ext 表在每 ACK 上的索引查找、双窗口 ACK 聚合的额外追踪、每 ACK 的 Kalman 更新计算。在微秒级 RTT 的数据中心环境下这些累积到了可观测的量级。
6. 结论
拥塞控制算法的调试和其他系统软件有一个本质区别:你不能断点,你不能暂停,你不能只看当前值。 你必须把整个反馈环作为一个完整的数学系统来推理。
正确的调试方法是:
- 打印采样,而不是打印变量。在每个决策点输出完整的计算链(输入 → 中间值 → 输出),和参考实现(BBR)在完全相同的格式下对比。
- 从偏差的末端反向推理。如果最终输出偏了,沿着计算链反向追踪到第一个偏差源。不要在中间层浪费精力。
- 数学公式手工验算 + AI 辅助交叉验证。对于复杂表达式(
mul_u64_u32_shr、do_div、div64_long),手工推导理论值,AI 辅助计算各种边缘情况和溢出可能性。两种方法结果一致时才确认排除。 - 逐特性整体回退。不要改代码逐行测——在反馈环里,单行修改的效果可能被其他未修改的部分掩盖。正确的方式是:从一个干净的基座开始,逐个叠加特性,每次测完整的 10 秒流量。
- 不要"判断",除非你真的可以。 内核 BBR 的 LT BW 超时恢复是一个不判断的设计——到期就恢复,不试图判断路径是否变好了。KCC 试图用峰值比均值来判断,结果判断永远为真。在闭环控制系统里,一个错误的判据造成的破坏远大于不做判断。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)