4、基于延迟的拥塞控制(Delay-based):到达时间滤波器、过载检测器、速率控制器、状态机迁移

聊到 WebRTC 的拥塞控制,很多人第一反应就是 GCC(Google Congestion Control)。而 GCC 里最核心、也最容易被忽视的部分,就是基于延迟的拥塞控制。说白了,它不是在等丢包了才反应,而是通过观察数据包到达时间的微小变化,提前判断网络是不是要“堵车”了。

我个人习惯把这一套机制叫做“网络交通预警系统”。它由四个模块组成:到达时间滤波器过载检测器速率控制器状态机。它们环环相扣,缺一不可。

核心思想: 如果网络没有拥塞,数据包的到达间隔应该和发送间隔基本一致。一旦到达间隔明显变长,说明网络里有人在“加塞”——也就是其他流量在抢带宽。

4.1 到达时间滤波器(Arrival Time Filter)

这个模块负责“清洗”原始数据。你想想看,网络延迟本身就有抖动,不能因为一次抖动就判定拥塞。所以我们需要一个滤波器,把噪声滤掉,提取出真正的趋势。

具体做法是:对每个视频帧(或音频包),记录它的发送时间到达时间,然后计算一个叫 inter-arrival jitter 的东西。嗯,这里要注意,不是简单的差值,而是用了一个线性回归的滤波器。

// 伪代码示意:到达时间滤波器
struct FrameGroup {
    int64_t send_time_ms;
    int64_t arrival_time_ms;
    int size_bytes;
};

double EstimateDelayGradient(const std::vector<FrameGroup>& groups) {
    // 用最小二乘法拟合延迟变化趋势
    double slope = LeastSquaresFit(groups);
    return slope; // 正数表示延迟在增加
}

我在项目中遇到过一个问题:如果视频帧率很低(比如 5fps),帧间间隔太大,滤波器会变得很不敏感。后来我们加了一个自适应窗口,帧率低的时候自动扩大观察窗口,才解决了这个问题。

4.2 过载检测器(Overuse Detector)

滤波器输出的是一个延迟梯度(delay gradient)。这个值如果是正的,说明延迟在增加;如果是负的,说明延迟在减小。但问题是:正多少才算“过载”?

这里有一个关键参数叫 阈值(threshold)。GCC 里这个阈值不是固定的,它会动态调整。为什么?因为网络环境在变,固定阈值要么太敏感(误报),要么太迟钝(漏报)。

延迟梯度值 判定结果 说明
gradient > threshold 过载(Overuse) 网络开始拥堵,需要降速
gradient < -threshold 欠载(Underuse) 网络空闲,可以提速
其他 正常(Normal) 保持当前速率

避坑指南: 我曾经把阈值设得太小,结果网络稍微波动一下就被判定为过载,导致码率频繁抖动。后来我参考了 GCC 的推荐值:初始阈值设为 12.5ms,然后根据当前网络状态在 [6ms, 25ms] 之间自适应调整。

4.3 速率控制器(Rate Controller)

过载检测器给出了“过载/欠载/正常”的判定,速率控制器就要据此调整发送码率。它用的是 AIMD(加性增乘性减)策略,但实现上有点讲究。

具体规则是这样的:

  • 过载状态: 码率乘以一个小于 1 的系数(比如 0.85),快速降速
  • 欠载状态: 码率加上一个增量(比如 8 kbps),缓慢提速
  • 正常状态: 保持当前码率,或者做微小的试探性增加

你可能会问:为什么不直接降到最低?因为我们要兼顾用户体验。降得太猛,视频会突然模糊;升得太快,又会引发新的拥塞。说白了,这是一个“试探-反馈-调整”的闭环。

// 速率控制器核心逻辑
uint32_t current_rate_kbps = 1000; // 初始 1 Mbps

void OnOveruse() {
    current_rate_kbps *= 0.85; // 乘性减
}

void OnUnderuse() {
    current_rate_kbps += 8; // 加性增
}

void OnNormal() {
    // 可以尝试微小增加,比如 +2 kbps
    current_rate_kbps += 2;
}

4.4 状态机迁移(State Machine)

最后,这四个模块不是各自为战的,它们通过一个状态机串联起来。状态机有三个状态:IncreaseDecreaseHold

状态迁移的规则我总结了一下:

  • Increase → Decrease: 检测到过载,立即降速
  • Decrease → Hold: 降速后,先保持一段时间,观察网络是否稳定
  • Hold → Increase: 如果网络稳定且有空闲带宽,尝试提速
  • Decrease → Increase: 如果降速后检测到欠载,说明降过头了,可以提前恢复

注意: 状态机里最容易出 bug 的地方是 Hold 状态的超时时间。设得太短,网络还没稳定就开始提速,会引发震荡;设得太长,带宽利用率太低。我一般建议 Hold 时间设为 1-2 秒,具体要看 RTT 大小。

为了让你更直观地理解整个流程,我画了一张图:

基于延迟的拥塞控制流程 到达时间滤波器 提取延迟梯度 过载检测器 判定过载/欠载/正常 速率控制器 调整发送码率 状态机 Increase Hold Decrease 过载 降速后稳定 有空闲带宽 欠载(降过头了) 反馈回路:码率变化影响延迟梯度

从这张图你可以看到,整个流程是一个闭环。到达时间滤波器输出延迟梯度,过载检测器判定网络状态,速率控制器调整码率,而码率的变化又会反过来影响延迟梯度。状态机则负责控制这个闭环的节奏,避免震荡。

嗯,这里要特别提一句:状态机里的 Hold 状态经常被初学者忽略。他们觉得“要么增要么减”,结果网络一波动就来回切换,用户体验很差。我个人习惯是在 Hold 状态里加一个计时器,至少等 1.5 秒再决定下一步。

最后总结一下:基于延迟的拥塞控制,核心就是“早发现、早处理”。它不需要等到丢包,通过延迟梯度就能提前感知网络压力。当然,它也不是万能的——如果网络里存在大量背景流量,延迟梯度可能会被淹没。这时候就需要结合基于丢包的拥塞控制来互补了。


公众号:蓝海资料掘金营,微信deep3321