开发者生态
morning
当“空闲”不空闲时:Linux 内核优化如何成为 QUIC bug
2026-05-13
1 阅读
sbulaev
当“空闲”不空闲时:Linux 内核优化如何成为 QUIC 错误 2026-05-12 Esteban Carisimo Antonio Vicente 10 分钟阅读 RFC 9438 中标准化的 CUBIC 是 Linux 中的默认拥塞控制器,因此控制公共互联网上的大多数 TCP 和 QUIC 连接如何探测可用带宽、在检测到丢失时退出以及随后恢复。在 Cloudflare,我们的 QUIC 开源实现 quiche 使用 CUBIC 作为其默认拥塞控制器,这意味着该代码位于我们所服务的大部分流量的关键路径中。在这篇文章中,我们将讲述一个错误的故事,其中 CUBIC 的拥塞窗口 (cwnd) 永久固定在最小值,并且永远不会从拥塞崩溃事件中恢复。故事始于 Linux 内核更改,旨在使 CUBIC 符合 RFC 9438 §4.2-12 中描述的应用程序限制排除 — 修复 TCP 中的一个实际问题,当移植到我们的 QUIC 实现时,会在乳蛋饼中出现意外行为。它有一个美好的结局:一个优雅的(近乎)单行修复打破了循环。 CUBIC 的逻辑简述 在我们深入探讨核心问题之前,快速回顾一下 CCA 可能有助于奠定基础。 CCA 的核心旋钮是拥塞窗口 ( cwnd ):发送方在任何时刻可以传输的字节数(已发送但尚未确认)的上限。较大的 cwnd 可以让发送方每次往返推送更多数据;较小的 cwnd 会限制它。每个基于损失的 CCA(包括 CUBIC)最终都是一种策略,用于在网络看起来健康时如何增长 cwnd,以及在网络不健康时如何缩小 cwnd。本质上,CCA 的目标是通过推断网络的“可用带宽”来最大化数据传输;因为没有人愿意为 1 Gbps 订阅付费,但只使用其中的一小部分。 CUBIC 属于基于丢失的算法系列,其运行的基本前提是:(1)如果没有数据包丢失,则提高发送速率(即提高带宽利用率); (2) 如果存在丢失,基于丢失的算法假定已超出网络容量,并且发送方必须后退(即降低带宽利用率)。这一逻辑建立在多年来经过重新审视的几个假设之上。不过,我们将在下次讨论这个问题。症状:测试失败率为 61% 我们的调查始于入口代理集成测试管道中意外失败的报告。这种不稳定的行为出现在测试中,在连接早期严重丢失的情况下评估了 CUBIC。拥塞崩溃后的恢复是一种不常见的情况,但这正是拥塞控制器要处理的情况。大多数拥塞控制测试都会执行算法的稳态和增长阶段;在连接被破坏后,探测以最小 cwnd 发生的情况的情况要少得多。状态空间这一角落的错误在吞吐量仪表板中是不可见的,静态审查无法检测到,并且只有当您故意将 CCA 驱动到其中并观察它是否可以爬出来时才会出现 - 这正是本次测试所做的。模拟测试设置包括以下详细信息: Quiche HTTP/3 客户端和服务器在本地 (localhost) 运行 RTT = 10ms(在配置中设置) 通过 HTTP/3 下载 10 MB 文件 使用 CUBIC 拥塞控制 在前两秒内注入 30% 的随机数据包丢失 两秒后,丢失完全停止 测试有 10 秒的超时时间来完成下载,预计将在四到五秒内完成 预期的行为很简单:CUBIC 应在丢失阶段,减少其拥塞窗口,一旦丢失停止,稳步增加并在超时内完成下载。相反,我们在多次 100 次运行中观察到,大约 60% 的测试无法在 10 秒的超时时间内完成下载。异常现象:999 次状态转换,零丢失 我们通过数据包丢失事件检测了 quiche 的 qlog 输出,并构建了可视化以了解拥塞控制器内部发生的情况:失败测试的连接概述。 T=2 秒后,数据包丢失完全停止,但 cwnd 仍固定在最低层,并且拥塞状态每约 14 毫秒在恢复和拥塞避免之间振荡。在两秒(2000 毫秒)标记之后,数据包丢失完全停止。然而,飞行中的字节数保持不变,这与 CUBIC 算法的核心逻辑相矛盾:在没有损失的情况下,应用更多的 Gas 来增加油门(我们的世界中有更多的字节)。这就提出了一个问题:如果网络不再丢弃数据包,为什么拥塞窗口无法增长?当我们放大该区域时,我们的分析显示 CUBIC 进入快速振荡,在我们的图中显示为延长的恢复阶段,介于拥塞避免状态(运行状态阶段)和恢复状态之间。