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 到达时间模型的完整流程

把上面这些串起来,整个流程是这样的:

  1. 接收端收到RTP包,按包组划分
  2. 计算每个包组的到达时间
  3. 与发送端的时间戳做差,得到延迟梯度d(i)
  4. 过载检测器用自适应阈值判断d(i)的状态
  5. 输出Normal/Overuse/Underuse信号
  6. 这个信号会传给码率控制器,决定是升码率还是降码率

下面这张图把整个流程画出来了,我建议你对照着代码一起看:

GCC基于延迟的拥塞控制流程 发送端 RTP 包组 网络传输 接收端包组到达 计算延迟梯度 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的基石,理解透了再看后面的丢包模型和码率控制器,就会轻松很多。