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_cwndkcc_update_bwkcc_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_aggquantization_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 = 24BBR_SCALE = 8BW_UNIT = 1 << 24 = 16777216BBR_UNIT = 256

对于 bw = 1358954min_rtt = 364gain = 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):

  1. 丢包发生时,开始 LT BW 采样(lt_is_sampling = true
  2. 收集 bbr_lt_intvl_min_rtts(4 个 RTT)的带宽数据
  3. 在下一个丢包时计算区间的 delivery rate
  4. 如果两个连续区间的带宽一致(差值 ≤ 1/8 或 ≤ 4 Kbps),且丢包率 ≥ 20%(bbr_lt_loss_thresh = 50,即 50/256 ≈ 19.5%),则判定为 policer
  5. 激活 lt_use_bw = 1,pacing_gain 锁为 BBR_UNIT(1.0x),cwnd_gain 不变(2x)
  6. 每 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.25xconsec_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_bwlt_use_bwlt_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 → 再次重置。循环往复。

这个振荡的后果:

  1. pacing_gainBBR_UNIT(1.0x)和 cycle gain(1.25x/0.75x/1.0x)之间高频切换
  2. 切换瞬间产生异常的 delivery rate 采样
  3. minmax 滑动窗口(非对称的 max 滤波器)捕获异常峰值,永不下落
  4. max_bw 被持久推高约 41%,稳定在一个新的、虚假的均衡态
  5. 这个虚假的 max_bw 进入 BDP 计算 → CWND 膨胀 → inflight 膨胀 → 丢包增加
  6. 更多的丢包又加剧了 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. 附带修复

在排查过程中发现的其他问题和修复(按发现顺序):

  1. 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。

  2. 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。

  3. 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 raws64 delta

  4. s64/u64 混合比较(u64)kcc->min_rtt_us 与 s64 delta 比较,有符号和无符号的混合在 delta 为负时产生错误结果。修复:去掉所有 (u64) 强转。

  5. 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

  6. ceil→floor:所有参数推导(high_gaindrain_gaincwnd_gainfull_bw_thresh)从 ceiling 除法改为标准截断除法,和内核 BBR 的定点整数运算行为一致。

  7. 类型对齐:函数签名 u32 gainint gainu64 bwu32 bw,匹配内核 BBR v5.4 的函数原型。

  8. kcc_set_state 对齐:删除冗余的 full_bw_cnt = 0packet_conservation = 0lt_bw_sampling 调用去条件判断,一字不差匹配内核 BBR。

  9. min_rtt 比较 <<=:内核 BBR v5.4 用 <=,KCC 用了 <<= 使相等的 RTT 也刷新 min_rtt 时间戳,减少不必要的 PROBE_RTT 触发。

  10. 死代码删除KCC_LT_BW_PROBE_RAMP_RTTSkcc_lt_bw_probe_pctKCC_LT_RESTORE_CNT_MAXkcc_lt_restore_ratio_num/denkcc_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. 结论

拥塞控制算法的调试和其他系统软件有一个本质区别:你不能断点,你不能暂停,你不能只看当前值。 你必须把整个反馈环作为一个完整的数学系统来推理。

正确的调试方法是:

  1. 打印采样,而不是打印变量。在每个决策点输出完整的计算链(输入 → 中间值 → 输出),和参考实现(BBR)在完全相同的格式下对比。
  2. 从偏差的末端反向推理。如果最终输出偏了,沿着计算链反向追踪到第一个偏差源。不要在中间层浪费精力。
  3. 数学公式手工验算 + AI 辅助交叉验证。对于复杂表达式(mul_u64_u32_shrdo_divdiv64_long),手工推导理论值,AI 辅助计算各种边缘情况和溢出可能性。两种方法结果一致时才确认排除。
  4. 逐特性整体回退。不要改代码逐行测——在反馈环里,单行修改的效果可能被其他未修改的部分掩盖。正确的方式是:从一个干净的基座开始,逐个叠加特性,每次测完整的 10 秒流量。
  5. 不要"判断",除非你真的可以。 内核 BBR 的 LT BW 超时恢复是一个不判断的设计——到期就恢复,不试图判断路径是否变好了。KCC 试图用峰值比均值来判断,结果判断永远为真。在闭环控制系统里,一个错误的判据造成的破坏远大于不做判断。

KCC 项目:https://github.com/liulilittle/kcc

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐