我从 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_headdrain加了安全超时- 引入了
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 思想的人,在遇到现实困境后,经过思考、失败、再思考,给出的一个我认为更诚实的回答。