GCC算法原理(上):基于延迟的拥塞控制,到达时间模型与过载检测
GCC(Google Congestion Control)是WebRTC最核心的流控算法。说白了,它就是一套让视频通话在「网络变差时不卡死,网络变好时不浪费」的智能调节机制。
我个人习惯把GCC拆成两大部分来看:基于延迟的拥塞控制和基于丢包的拥塞控制。这一章我们先啃前半部分——延迟模型。你想想看,网络还没丢包的时候,延迟其实是最早的预警信号。
4.1 为什么选择延迟作为拥塞信号?
我在项目中遇到过不少这样的场景:网络已经出现瓶颈了,但丢包率还是0%。这时候如果只靠丢包来判断,流控根本不会触发。等到丢包出现,其实拥塞已经比较严重了。
延迟信号的优势在于:
- 更敏感——队列开始堆积,延迟立刻上升
- 更平滑——不像丢包那样跳变剧烈
- 更及时——可以在丢包发生前就做出调整
核心观点:延迟上升是网络拥塞的「前兆」,丢包是「结果」。GCC的延迟模型就是要在结果出现之前,先感知到前兆。
4.2 到达时间模型(Arrival Time Model)
GCC怎么知道延迟变了?它靠的是到达时间模型。这个模型不关心绝对时间,只关心相对变化。
具体来说,它比较两样东西:
- 发送间隔——两个包组在发送端的时间差
- 到达间隔——这两个包组在接收端的时间差
如果到达间隔 > 发送间隔,说明网络出现了排队延迟。嗯,这里要注意,这个比较不是一次性的,而是基于包组(packet group)来做的。
4.2.1 包组的概念
为什么用包组而不是单个包?我刚开始看代码时也有这个疑问。后来调试才发现,单个包的延迟抖动太大了,根本没法用。把连续发送的一批包打包成一个组,取它们的平均到达时间,噪声就小多了。
包组的划分规则:
- 发送时间间隔小于5ms的包归为一组
- 每个包组计算一个到达时间戳
- 包组大小一般不超过几十个包
4.2.2 延迟梯度计算
到达时间模型的核心公式其实很简单:
延迟梯度 d(i) = (t_i - t_{i-1}) - (T_i - T_{i-1})
其中:
t_i = 第i个包组的到达时间
T_i = 第i个包组的发送时间
d(i) > 0 表示延迟在增加
d(i) < 0 表示延迟在减少
这个d(i)就是过载检测的输入信号。我在项目中调试过很多次,d(i)的曲线能非常直观地反映网络状态的变化。
调试技巧:在WebRTC的日志中搜索"DelayBasedBwe",可以看到实时的d(i)值和过载检测结果。我建议你在测试时把这个日志打开,配合网络模拟工具(比如NetEm)一起看,效果非常好。
4.3 过载检测器(Overuse Detector)
有了d(i)之后,怎么判断网络是不是过载了?GCC用了一个自适应阈值的方案。
你想想看,如果用一个固定阈值,网络波动大的时候就会频繁误判。所以GCC的阈值是动态调整的:
- 当延迟梯度持续为正,阈值会逐渐降低,变得更敏感
- 当延迟梯度稳定在零附近,阈值会逐渐升高,变得更宽容
4.3.1 状态机
过载检测器内部维护了一个三状态的状态机:
| 状态 | 含义 | 触发条件 |
|---|---|---|
| Normal | 网络正常,无拥塞 | d(i) 在阈值范围内 |
| Overuse | 网络过载,需要降速 | d(i) 超过阈值且持续一段时间 |
| Underuse | 网络空闲,可以提速 | d(i) 明显为负值 |
我曾经踩过一个坑:在弱网环境下,Underuse状态频繁触发,导致码率一直往上冲,然后又因为Overuse被降下来,形成剧烈震荡。后来发现是阈值调整参数没配好。
避坑指南:我曾经在某个项目中把阈值调整速度设得太快,结果过载检测器变得极度敏感,稍微有点抖动就触发Overuse。建议阈值调整的步长不要超过0.5ms/次,调整间隔至少100ms。
4.4 到达时间模型的完整流程
把上面这些串起来,整个流程是这样的:
- 接收端收到RTP包,按包组划分
- 计算每个包组的到达时间
- 与发送端的时间戳做差,得到延迟梯度d(i)
- 过载检测器用自适应阈值判断d(i)的状态
- 输出Normal/Overuse/Underuse信号
- 这个信号会传给码率控制器,决定是升码率还是降码率
下面这张图把整个流程画出来了,我建议你对照着代码一起看:
4.5 关键参数与调优建议
在实际调试中,有几个参数我建议你重点关注:
| 参数名 | 默认值 | 作用 | 调优方向 |
|---|---|---|---|
kOverUsingTimeThreshold |
10ms | 触发Overuse的延迟梯度阈值 | 网络抖动大时适当增大 |
kUp |
0.01 | 阈值上升速度 | 需要更敏感时减小 |
kDown |
0.00018 | 阈值下降速度 | 需要更稳定时增大 |
包组时间间隔 |
5ms | 包组划分的时间窗口 | 低码率场景可适当增大 |
个人经验:在移动网络环境下,我习惯把kOverUsingTimeThreshold从10ms调整到15ms。因为4G/5G的延迟抖动本身就比有线网络大,阈值太低了容易误触发。
4.6 常见问题与排查思路
最后分享几个我在调试中遇到过的问题:
- 频繁Overuse导致码率过低——先检查是不是阈值太敏感,再看网络本身有没有突发抖动
- Underuse后码率回升太慢——通常是阈值下降速度kDown设得太小,导致状态切换迟钝
- 延迟梯度一直为正但码率不降——检查过载检测器的状态机是否被卡住,或者阈值调整逻辑有bug
嗯,基于延迟的拥塞控制就讲到这里。这部分是GCC的基石,理解透了再看后面的丢包模型和码率控制器,就会轻松很多。