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)
最后,这四个模块不是各自为战的,它们通过一个状态机串联起来。状态机有三个状态:Increase、Decrease、Hold。
状态迁移的规则我总结了一下:
- Increase → Decrease: 检测到过载,立即降速
- Decrease → Hold: 降速后,先保持一段时间,观察网络是否稳定
- Hold → Increase: 如果网络稳定且有空闲带宽,尝试提速
- Decrease → Increase: 如果降速后检测到欠载,说明降过头了,可以提前恢复
注意: 状态机里最容易出 bug 的地方是 Hold 状态的超时时间。设得太短,网络还没稳定就开始提速,会引发震荡;设得太长,带宽利用率太低。我一般建议 Hold 时间设为 1-2 秒,具体要看 RTT 大小。
为了让你更直观地理解整个流程,我画了一张图:
从这张图你可以看到,整个流程是一个闭环。到达时间滤波器输出延迟梯度,过载检测器判定网络状态,速率控制器调整码率,而码率的变化又会反过来影响延迟梯度。状态机则负责控制这个闭环的节奏,避免震荡。
嗯,这里要特别提一句:状态机里的 Hold 状态经常被初学者忽略。他们觉得“要么增要么减”,结果网络一波动就来回切换,用户体验很差。我个人习惯是在 Hold 状态里加一个计时器,至少等 1.5 秒再决定下一步。
最后总结一下:基于延迟的拥塞控制,核心就是“早发现、早处理”。它不需要等到丢包,通过延迟梯度就能提前感知网络压力。当然,它也不是万能的——如果网络里存在大量背景流量,延迟梯度可能会被淹没。这时候就需要结合基于丢包的拥塞控制来互补了。