拥塞控制:排水终止的两种决策:OR 与 AND
PROBE_BW 的周期由 8 个相位构成:6 个巡航(1.0x pacing)、1 个上探(1.25x)、1 个下探(0.75x)。下探的任务是在上探将 inflight 推到 BDP 之上后,以低于瓶颈速率的 pacing 使多余 inflight 消退。下探的终止条件决定了消退进行到什么程度才认为完成。
终止条件对外暴露的代码量很小——只有一行 OR 或一行 AND——但其背后所对应的假设、信任分配和失败模式,构成了两个拥塞控制实现之间最集中的分岔点之一。以下不是关于哪条路更好的争辩——而是逐一展开设计者所面对的权衡空间,以及各自认为合理的取舍。
一、OR 门:以时间为尺的排水
条件设定
BBR 的原始排水终止条件为is_full_length || drained。is_full_length定义为从排水相位开始至今已经历至少一个 min_rtt(使用>而非>=,以确保相位在时序边界上不会过早结束)。drained定义为当前在途数据量(inflight at EDT)已经降至 1.0x pacing 增益下的 BDP 估计。
满足二者之一,排水即告终止。
设计者的推理路径
这个设计建立在一个对 BDP 模型的信任上:如果 min_rtt 是准确的,max_bw 是准确的,那么inflight ≤ BDP在物理上等价于 bottleneck buffer 为空。不需要进一步验证——模型本身已经给出了排水的数学条件。
如果模型是可信的,那么在任何时间点上,inflight 降至 BDP 这一条件本身就足以确认排水完成。但模型的可信度不是绝对的:min_rtt 窗口可能在排水期间采样到一个新的更低值;max_bw 可能被排水阶段的低 inflight 压低。is_full_length的另一个角色是为模型的不确定性提供时间上限——即使模型在排水期间偏离,最多也只浪费一个 RTT 的排水时长。
因此 OR 门的逻辑可以读作:一个 RTT 之后,无论模型是否已确认排水完成,排水都结束。选择持续排水更长的时间能获得更准确的 BDP 确认——但设计者认为模型本身已经足够好,不需要这个额外的确认。
定时排水的合理性
单流路径上,一条流在上探产生的多余 inflight 量是确定的。上探以 1.25x pacing 运行一个 min_rtt,inflight 从大约 1.0 BDP 升至约 1.25 BDP。产生的超额是 0.25 BDP。下探以 0.75x pacing 运行,一个 min_rtt 的 infight 降幅约为 0.25 BDP——恰好等于上一相位的超额。在这个理想模型下,一个 min_rtt 是排水的自然时间长度。
没有进一步的验证——不需要等待drained的实际确认——不是因为设计者看不清潜在的偏离,而是在这个特定的路径假设下(单流、稳定 BDP、不重叠上探),上探的多余 inflight 与排水速率形成一个精确的 1:1 抵消关系。等待额外的确认不会改变结果,只会释放已排空的 buffer 后被更慢的速率继续占用。
有意的时序偏差
BBR 不使用delta >= min_rtt而使用delta > min_rtt的判断方式。>=允许在恰好满一个 min_rtt 的 ACK 到达时立即终止排水——这个 ACK 本身可能仍然反映着排水刚启动时的 inflight 状态(因为 ACK 的反馈有至少一个 RTT 的延迟),此时drained的判断基础是偏早的。>强制相位至少多等一个 ACK——不是逻辑必须的,是一个工程上的余量。
二、多流集体队列的形成
共享瓶颈上的上探叠加
当 N 条独立 BBR 连接共享一个物理瓶颈时,每条流的上探相位随机分布于它们的独立 8-相位周期中。在任何给定的时刻,期望有 N/8 条流处于上探状态(1.25x pacing),剩余的流分布在巡航(1.0x)和下探(0.75x)之间。聚合 pacing 产出的 inflight 为:
聚合 inflight = (N/8) × 1.25 BDP_per_flow + (6N/8) × 1.0 BDP_per_flow + (N/8) × 0.75 BDP_per_flow简化后约为 N × 1.0 BDP + N/8 × 0.25 BDP。超出了瓶颈容量 N × BDP 的那部分多出来的 0.25 BDP 条数的累积,不来自任何一条流单独的 pacing,而来自它们在同一时间间隔内的独立上探被叠加到了同一个瓶颈 buffer 中。
时间折叠
在一个 200ms RTT 的路径上,一个 PROBE_BW 周期持续 8 × 200ms = 1.6 秒。上探相位占据 200ms——每条流在这 200ms 内独立推送 1.25x inflight。N 条流的相位各不同步——它们在不同时间点开始和结束上探。但瓶颈 buffer 不区分"谁的" inflight——它只看到总 inflight 的瞬时值。在时间折叠下,任何时候都有多个流在推动超过其稳态份额的 inflight,buffer 中的集体队列大于任何一个流自己估计的 BDP。
OR 门在此路径上的行为
各条流在自己的周期中各自运行排水——各自进入下探相位,独立执行is_full_length || drained。is_full_length条件在每条流的本地时钟满一个 RTT 时触发——不检查全局的 inflight 状态、不比较其他流的队列进展。N 条流的is_full_length在时间上有先后,但整体在一个比较窄的窗口内依次触发。
此时任何一条流的drained条件几乎肯定不成立——集体队列远超该流的独立 BDP 估计。在 OR 门下,is_full_length先于drained成立,排水退出。但此时集体队列的总量并没有减少到瓶颈容量以下——它只是被一轮排水部分削减了而已。下一轮巡航以 1.0x pacing 运行,inflight 在此水平上维持——不增不减。再下一轮上探再次堆叠到了已经存在的残留队列上,CWND 在连续几个周期内不断爬升直到触发丢失。
这个过程不是任何一个单条流的失误——是 OR 门对超越单流的聚合 inflight 没有提供结构性的感知。
三、AND 门:以物理量为尺的排水
条件替换
KCC 将终止条件替换为(is_full_length && drained) || safety_timeout。和 OR 门的区别仅在于中间的||变成了&&——然而这一个运算符的改变,把排水从基于时间的定时退出改为了基于测量值的验证退出。
为什么设计者认为这样更合理
AND 门背后的判断是:时间本身不是排水完成的有效证据。时间过去了只说明等待了多久,不说明队列是否真的已经清空。因为外部队列来源——其他流的 inflight、其他 CC 算法的流量、ACK 压缩效应——都会使 inflight 的下降速率偏离 BDP 模型预测的线性。drained是对现实的确认,is_full_length是对确认之前的必要等待的约束。两个同时成立,才构成一个完整的"排水已完成"的断言。
这个设计不假设 BDP 模型一定能跟上队列的实际变化。它把模型交给实际测量去验证——它更谨慎,但代价是等待时间更长。
实现
bool drained = kcc_packets_in_net_at_edt(sk, rs->prior_in_flight, etd_bw) <= kcc_inflight(sk, max_bw, BBR_UNIT, ext); return (is_full_length && drained) || delta > kcc->min_rtt_us * KCC_DRAIN_TARGET_MAX_RTTS;kcc_packets_in_net_at_edt是 BBR 的对 in-flight 的 EDT 感知估计——它计算从瓶颈发送到 ACK 返回这一段时间内在路上还有多少数据。kcc_inflight(sk, max_bw, BBR_UNIT, ext)计算的是 1.0x 增益下 BDP 的目标 inflight。对比的逻辑是:在途数据量是否已经降到了目标 BDP 之下。
这个比较不会在is_full_length成立之前进行——is_full_length仍作为必要条件,因为在一个 RTT 之内,ACK 的反馈还不反映排水开始之后的 inflight 下降,测量值是滞后的。
多流收敛速度
在 8 流、1 Gbps 瓶颈、约 200ms min_rtt 的路径上,上探的集体队列可能使每条流的 inflight 在峰值触及 2.0-2.5 BDP。从峰值排到 1.0 BDP 的过程有几个 min_rtt 的长度——is_full_length在一个 RTT 后成立,但drained要等到再多 1-2 个 RTT 后才成立。AND 门在这些额外的轮次中保持 0.75x pacing,直到 inflight 确实回降到 1.0x 水平。
四、安全超时
触发条件
delta > kcc->min_rtt_us × KCC_DRAIN_TARGET_MAX_RTTS(默认 4 个 min_rtt)在 AND 门等待超过 4 个 min_rtt 后无条件终止排水。
单独存在的必要性
AND 门的结构中没有天然的时间终止——如果drained永远不成立,排水将永不终止。这种情形不是理论上的——在瓶颈被永久竞争流量占据时(例如一个非 BBR 连接以恒定速率占据 buffer),AND 门看不到任何使 inflight 降到 BDP 之下的机会,继续 0.75x 排水只会使 inflight 降到不合理的低值,造成 self-starvation。
安全超时是这条路径上的保底策略。它不等同于"排水已完成"——它等同于"排水已放弃"。相位被强制切换到 cruise,让 1.0x pacing 在正常的 cycle 中重新开始竞争。
4 个 RTT 的选择
这不是一个基于精密度量的常数。选择 4 是出于对竞争持续性的经验估计——在 RTT 200ms 的条件下,4 个 RTT 大约 800ms,覆盖了绝大多数"排队确实在递减、只是比较慢"的场景(2-3 个 RTT)并留出一个缓冲轮次。不是一个最优解,是一个不会过早也不会过晚的经验中点。
五、Kalman drain-skip:上探后无队列即不排水
AND 门的补充策略
AND 门的确认延迟在单流路径上是纯粹的空等——一条流的上探队列在一个 RTT 内就已经自己排完了,drained条件在is_full_length成立的同一时刻就已经为真。AND 门却仍要同时检查两者,结构上的确认延迟对单流场景没有提供任何新的信息。
KCC 的应对不是修改 AND 门本身,而是在排水条件评估之前增加一条单独的检查——在特定的信号组合下,整个排水直接被判定为"不需要",下探相位当巡航处理。
三个信号
Kalman 已收敛:
ext->p_est < kcc_kalman_converged_p_est_val(默认 500)。误差协方差 p_est 降至收敛阈值以下说明 RTT 估计已达到稳态——没有未解释的偏差来源。Queue delay 接近零:
ext->qdelay_avg < kcc_drain_skip_qdelay_us_val(默认 1000μs)。Kalman 估计的 average queue delay 小于 1ms——表明 buffer 内部几乎没有排队累积。它由 Kalman 的传播延迟估计与 raw RTT 样本之间的差值得出,相比直接观察 RTT sample 对单个 min_rtt 的变化敏感度差,但在统计上比单一样本更稳定。与上次排水的间隔充足:
delta > kcc->min_rtt_us / KCC_DRAIN_SKIP_MIN_RTT_DIV(默认为 min_rtt 的八分之一)。防止在连续的瞬时 ACK 中没有经过充分的时间间隔即再次触发第一和第二个条件。它不是排水状态判断的一部分——它保护判断本身不受 ACK 节奏的干扰。
三个信号共同指定的含义
当 Kalman 说明 RTT 估算是稳定的(p_est 低),并且 buffer 几乎是空的(qdelay 低),那么可以推断出上探的 1.25x pacing 在这一轮没有产生一个显著的队列。如果上探没有产生显著队列——排水的理由就不存在。不需要一个额外的逻辑来确认或否认这一点。排水直接跳过。
这里存在一个 Kalman 收敛后的隐含信任——收敛后的状态推断是可靠的。如果对 Kalman 的估计准确性有保留,那么这个跳过是不安全的。但算法的设计者做了一个判断:收敛后的 RTT 估计在各条路径上的可靠性足以支持这个跳过。
六、OR 门的结构偏重
BDP 模型的自证
OR 门把排水的判定权交给了 BDP 的模型推算值。在模型可信的范围内,这产生了一个简洁有效的行为——排水周期长度固定、可预测、不需要外部 observability。BBR 的带宽和 RTT 估计是共享一个滑动窗口的,一致性在其内部是保证的。
但这种自证只能在单流路径上完整成立。当 BDP 的估计因为外部队列的存在而发生偏移时——max_bw 在多流竞争中被压低,min_rtt 仍然保持——BDP 的推算值偏低。此时drained判据已经不再反映真实的瓶颈状态——它是自证的闭环。OR 门对此没有保护——因为它不期望超出模型解释能力的外部事件发生。
这既不是设计的漏洞,也不是模型本身的缺陷——在设计者的假设空间内,外部的聚合队列是分配模型的低概率场景。对一个通用算法来说,将设计建立在"会有人帮我处理外部性"的隐含前提上是一个简化的选择——不是错误的,但不是一个不面临这种场景的选择。
可预测性的成本
定时排水产生了一个行为可预测的循环。对于线上监控、参数调优、吞吐分析而言,每个相位的时长是常数,行为的任何偏差都可以从样本中检测。OR 门的这个特性对运维是有价值的。
但同时,可预测性意味着它不会对现实中的不可预测做出结构性的应对。这个交换是合理的——不是因为它总对,是因为设计者认为在绝大多数部署环境中,不可预测的聚合队列的出现频率低于可预测周期带来的好处。
七、AND 门的时间成本
确认延迟的分解
AND 门中drained在is_full_length成立后的额外等待来自于测量 lag——当is_full_length成立时,对应着排水启动时的数据的 ACK 刚刚返回。此时 inflight 的 EDT 估计可能正在下降但尚未在 DSP 上反映。到drained成立为止的额外延迟包含两个成分:ACK 返回的反馈延迟和实际排水所需的物理时间。
在 RTT 为 5ms 的单流链路上,这个额外延迟通常在 1-5ms 范围。对于一个 8 相位、总长约 40ms 的完整 PROBE_BW 周期,额外的 1-5ms 约为 2.5%-12.5% 的额外周期时间。这不是一个可忽视的量——它足够大到可以在吞吐基准测试中产生可观测的差异。
Kalman drain-skip 的成本回收
在这条单流链路上,当 Kalman 三个条件同时成立时,排水被完全不执行。AND 门的确认延迟不存在——没有产生过排水,没有需要确认的。干净链路上的单流传输在 Kalman 收敛后的第一个上探相位之后即隐式完成排水,下探不产生额外时间成本。
AND 门在多流碰撞时的确认延迟是在产生实际价值的(等待真实队列排空),而在单流无碰撞时被跳过——成本和收益的结构是对称的。
八、两种判断体系的并置
将kcc_is_next_cycle_phase的完整源码展开,可以看到排水终止的决策层级:
- Kalman drain-skip:三个条件满足 → 判为已完成 → 不执行排水
- AND 门 + 超时:两个条件都满足 → 排水完成;否则在 4 个 RTT 后强制结束
- 纯巡航:
is_full_length单一条件 → 经过一个 RTT 即完成
这三个层级在一个函数中顺序求值,各自维护各自的条件集合,互不假设对方的状态。实际运行中哪一层被激活完全由实时测量决定——与设计者的选择无关。三层都是可单独操作的判断单元,组合使用而不是替代。
九、合理性而非完美性
OR 门和 AND 门都是在某个特定假设下被判定为合适的排水机制。OR 门的设计者认为模型是充分的——BDP 的自身定义提供了足够的信息来判断排水何时完成。AND 门的设计者认为需要观察——等待外部测量确认 inflight 确实降到了预期的水平。
没有一条路能在所有假设下都表现出色。OR 门在多流碰撞下会经历队列堆积,AND 门在稳定单流下付出额外的等待时间。每一种设计选择了自己的侧重点——没有一个是"解决问题的那个",但各自解决了一个层面的不确定性。
选择 AND 门不是因为 OR 门有什么可以被替代的设计缺陷。是设计者看到了一个在多流 VPS 场景中频繁出现的路径条件、判断其值得一个专用的策略路径。OR 门的设计者对这个问题可能也有自己的处理办法——可能在一个不同的位置,可能选择接受它。两种选择都是合理的。
在 TCP 拥塞控制的长期经验中,一个合理的设计往往在它自身的假设下表现良好,在不同的假设下出现不同类型的故障。设计者的工作是理解这种选择,而非撤销它。
Tags: TCP, BBR, KCC, drain, cycle phase, multi-flow, congestion control, algorithm design