我从 BBRv1 到 KCC 的思考

我从 BBRv1 到 KCC 的思考

1. 一开始,我只是想要一个可以解释的算法

我刚接触拥塞控制的时候,CUBIC 还是主流。它工作得还行,但我一直有个不舒服的感觉:它把丢包当作拥塞的唯一信号。可丢包明明有很多原因------浅缓冲区、无线干扰、甚至 CPU 调度抖动。你都不知道自己到底是因为网络满了才丢包,还是因为别的什么原因。

那时候 Reno、CUBIC 这类算法的逻辑是:丢包 → 砍窗口 → 慢慢恢复。像一个被烫了手就缩回去、然后再试探的孩子。你能说它不对吗?也不能。但总觉得少了点什么------少了那种"我知道我为什么这么做"的确定性。

直到 2016 年,BBRv1 出来了。

说实话,第一次读 BBR 论文的时候,我心里有一种"对,就该是这样"的感觉。它不盯着丢包,而是去测量两个物理量:带宽和 RTT。然后算出 BDP,就让发送行为绕着 BDP 转。丢包只是探测过程中可能出现的结果,不是需要恐慌的信号。

这套逻辑是可以解释的。你问一个 BBR 流现在为什么降速?它可以告诉你:因为我测到的 RTT 变大了,说明队列在涨,所以我调低增益。你问它为什么加速?因为我发现带宽还有余量。

BBR 的优雅在于它建立了一个因果链条:测量 → 状态估计 → 决策。而不是"丢包了,砍窗口"------因果链太短,信息损失太大。

所以我成了 BBR 的信徒。准确说,是 BBRv1 的信徒。

2. 但 BBRv1 有它的命门

用了一段时间 BBRv1,我开始在实际场景中碰到问题。

比如多流共享一个瓶颈时,八个 BBR 流会同步震荡。比如和 CUBIC 共存时,BBR 反而容易被挤占。比如在 LTE 网络里,RTT 经常跳来跳去,BBR 的 min_rtt 窗口要么跟不上,要么被污染。

问题的根源在哪里?我后来想明白了:BBRv1 对 RTT 的假设太强了

BBRv1 认为:真实传播延迟 ≈ 所有 RTT 样本的最小值。因为排队延迟是非负的,噪音可以被窗口平均掉,所以最小值最接近真相。

这个假设在实验室里成立。但在现实网络中:

  • 你永远不能保证窗口期内真的出现过一次零排队的样本。
  • 噪音不是零均值的,也不是对称的。无线突发的延迟抖动、ACK 压缩、时钟漂移,都会让最小值也不那么"真"。
  • 当真实传播延迟因为路由切换而变大时,滑动窗口的最小值要等旧窗口过期才能上升,这期间 BDP 低估,吞吐量直接腰斩。

所以 BBRv1 的命门是:它试图用一个非常简单的工具(滑动窗口取 min)来撬动一个非常复杂的问题(从混叠信号中分离真实值)。它在大多数时候够用,但碰到噪声多、路径变化快的场景,就露怯了。

3. BBRv2/v3 让我感到失望

我知道 BBR 团队肯定也看到了这些问题。后来 BBRv2 和 BBRv3 陆续出来,我原本期待他们能找到一种更优雅的方式来解决 BBRv1 的测量缺陷。

但我看到的不是对测量模型的改进,而是规则的堆砌

  • probe_up 退出条件增加了 is_app_limited!tcp_send_head
  • drain 加了安全超时
  • 引入了 loss_work_area,把丢包重新当作信号
  • ECN 被当作另一个触发开关,配上各种阈值(比如 ECN 标记率超过 5% 就降窗口)

每发现一个新问题,就加一条新规则。我理解这种务实的动机------线上服务不能崩,KPI 要交差。但从技术美学的角度,这让我很难受。

因为规则堆砌本质上是在绕过问题,而不是解决问题

核心问题依然在那里:你还是无法从 RTT 样本中分离出真实传播延迟、排队延迟和噪音。你只是用越来越多的状态变量去掩盖分离失败带来的副作用。规则越多,算法行为越难预测,越像一个"在这个测试集上跑得不错"的黑盒,而不是一个可以逻辑推导的数学模型。

更让我失望的是,BBRv2/v3 事实上否定了 BBRv1 最核心的理念------丢包不是问题。把丢包重新请回来,在我看来是一种倒退。

我开始怀疑:BBRv2/v3 的主导者可能已经不是原班人马了。Van Jacobson、Neal Cardwell 那批人的学术纯洁性,在工业化压力下被稀释了。这不是指责谁,是大型项目演进的常见命运。

4. 我自己也走过弯路:UCP

当时我觉得,既然 BBRv1 的通用模型不够鲁棒,那我就针对不同网络类型做定制。UCP(Unbiased Congestion Predictor)的想法是:先判断用户在哪类网络上(有线/无线、低延迟/高 RTT、数据中心/公网),然后给每类网络配一套最优的增益表和规则。

听起来合理对吧?但做起来全是坑。

问题出在:分类本身需要的特征,恰恰是我无法直接观测的。我需要知道真实传播延迟和排队延迟才能判断网络类型,而这两者正是我要估计的东西。这是一个循环依赖。

我只能用一些粗糙的启发式去"猜"类别。比如 RTT 方差大 → 无线;RTT 绝对值大 → 卫星。但启发式在 A 网络有效,到 B 网络就因为噪声分布不同而失效。于是我陷入"如果抖动高则调阈值"、"如果排队异常则换增益表"的无休止补丁循环。

最终 UCP 死了。不是因为工程能力不够,而是因为这条路在逻辑上就走不通。试图对网络进行分类是徒劳的,因为类别是人为划分的、不稳定的、而且无法直接观测。

这个失败让我痛苦了很久,但也让我彻底明白了一件事:不要试图用规则去覆盖世界的多样性,而要建立一个能适应多样性的模型。

5. KCC:回归模型,用卡尔曼滤波测量

UCP 失败后,我开始寻找能真正解决核心命题的工具。卡尔曼滤波进入了我的视野。

卡尔曼滤波的基本模型:

复制代码
x[k] = x[k-1] + w          // 真实传播延迟随机游走
z[k] = x[k] + q[k] + v     // 观测 = 真实 + 排队 + 噪音

这个模型直接对应了我之前说的三个变量:

  • x 是真实传播延迟(我要估计的状态)
  • q 是排队延迟(非负,可以从残差中剥离)
  • v 是零均值测量噪音(被建模为协方差 R)

卡尔曼滤波不能魔法般地把三者完美分离,但它能做到:

  • 给出 x 的最优估计(最小均方误差)
  • 同时给出估计的不确定度 p_est
  • 每次更新后,q_hat = max(0, z - x_hat) 就是排队延迟的估计
  • 噪音协方差 R 可以动态调整(比如 ECN 来了,说明当前观测可信度高,就降低 R)

这个框架让我激动。因为它正面回答了核心命题,而不是绕过它。它承认我无法完美分离三者,但可以用概率模型去估计它们,并告诉我估计有多可靠。

于是我决定:保留 BBRv1 的状态机框架(STARTUP、DRAIN、PROBE_BW、PROBE_RTT),因为那个框架在结构上仍然是正确的。我只替换其中最脆弱的一环------min_rtt 的估计方法。

这就是 KCC 的诞生。

6. 模型改进带来的自然衍生

替换为卡尔曼滤波后,几个长期困扰我的问题自然地得到了解决。注意,是自然衍生,而不是我刻意去打补丁。

PROBE_RTT 不再是必需品:因为卡尔曼本身就在持续收敛到真实传播延迟,不需要周期性强制 drain 去刷新 min_rtt。那个每 10 秒一次的吞吐悬崖可以被移除。

排队延迟和抖动变成了有意义的信号:以前它们只是噪声,现在我可以根据 q_hat 来动态调整 PROBE_BW 阶段的增益,根据 jitter 来调节卡尔曼的 R,甚至用低 qdelay 和低 jitter 来检测单流场景。这些都是从模型内部自然衍生出来的。

与 CUBIC 共存时更稳健:CUBIC 会造成队列波动,污染 RTT 样本。但卡尔曼对真实传播延迟的估计比滑动窗口最小值更鲁棒,不容易被带偏。我不需要写任何"识别 CUBIC 然后退让"的丑陋规则。

ECN 被优雅地融入:交换机发出的 CE 标记,本质上是对排队延迟过大的提前警告。在卡尔曼框架里,这告诉我当前观测的噪声很低,应该降低 R,让滤波器更相信这次观测。ECN 变成了数学矩阵里的一个参数调节信号,而不是一个 if-else 分支。

我也在 KCC 里加了一些辅助改进,比如 DRAIN 退出的 AND+超时、PROBE_RTT 的抖动偏移、LT 采样的拥塞门控等。但这些都不是"补丁"------它们是模型改进后,为了让状态机适配新模型而做的合理调整。如果你仔细看,它们每个都有理论依据,而不是拍脑袋的经验阈值。

7. 我的态度

我不认为 BBRv2/v3 是"错误的"。它们只是选择了另一条路------一条更务实、更工程化、但在我看来也更平庸的路。我尊重那个选择,但我不会走那条路。

我也不认为 KCC 是完美的。它还有很多可以改进的地方:比如从单状态卡尔曼扩展到带宽-延迟联合估计,或者引入更强大的滤波来处理高度非线性的无线链路。完美不存在,方向才重要。

我选择的方向是:正面回答核心命题,用模型驱动代替规则驱动,用概率滤波代替经验阈值

BBRv1 给了我一个起点。KCC 是我从这个起点出发,用自己的方式往前走的一步。

这不是一个"终极答案"。这是我作为一个喜欢 BBRv1 思想的人,在遇到现实困境后,经过思考、失败、再思考,给出的一个我认为更诚实的回答。

相关推荐
落羽的落羽1 小时前
【项目】JsonRpc框架——开发实现1(细节功能、字段定义、抽象层、具象层)
linux·服务器·网络·c++·人工智能·算法·机器学习
shixuzhimeng1 小时前
FTP服务器项目
linux·网络·ftp
handler011 小时前
【算法】并查集(普通/扩展/带权)模板与例题
数据结构·c++·笔记·算法·c·图论·查并集
Latticy2 小时前
内网渗透-横向移动-密码喷洒攻击和域内用(kerbrute使用)
运维·服务器·网络·内网渗透·内网
繁星蓝雨2 小时前
C++中对比pragma once和ifndef的使用区别
开发语言·c++·ifndef·头文件·pragma once
.千余2 小时前
【C++】C++手写Vector容器:从底层源码模拟实现
开发语言·c++·经验分享·笔记·学习
a诠释淡然2 小时前
C++ vs Rust:哪个更适合你的下一个项目?
开发语言·c++·rust
小小de风呀2 小时前
de风——【从零开始学C++】(十二):stack和queue的基本使用和模拟实现
开发语言·c++