2. GCC算法深度解析:基于延迟的拥塞控制(Delay-based Control)的工作原理

GCC(Google Congestion Control)是WebRTC的默认拥塞控制算法。它分为两条腿走路:一条基于延迟,一条基于丢包。今天我们先聊基于延迟的那条腿——Delay-based Control。

说白了,这条腿的核心任务就是:通过观察数据包到达时间的波动,判断网络是否开始拥堵,然后主动调整发送速率。它不需要等到丢包发生,反应比丢包检测快得多。

2.1 核心思想:排队延迟 = 拥塞信号

网络拥塞的本质是什么?是数据包在路由器缓冲区里排队。排队就会产生额外延迟。GCC的Delay-based Control就是利用这个排队延迟作为拥塞信号。

我刚开始接触这个算法时,觉得它很巧妙——它不依赖丢包,而是靠时间信息。你想想看,丢包是网络已经撑不住的信号,而排队延迟是网络快要撑不住的信号。前者是事后,后者是事中。

关键公式:

排队延迟 = 实际到达时间 - 期望到达时间

当排队延迟持续增大 → 网络正在拥塞 → 降低发送速率

当排队延迟持续减小 → 网络空闲 → 提高发送速率

2.2 算法流程:从包组到趋势判断

GCC的Delay-based Control大致分三步走:

  1. 包组(Packet Group)划分:将连续发送的包分成组,计算每组的一路延迟变化
  2. 延迟梯度计算:用到达时间差减去发送时间差,得到延迟变化量
  3. 趋势检测:用卡尔曼滤波器或趋势线判断延迟是在上升还是下降

嗯,这里要注意:包组不是随便分的。GCC会把发送时间间隔小于5ms的包视为同一组。为什么是5ms?我个人习惯把这个值理解为“网络的最小时间分辨率”,太细了噪声大,太粗了反应慢。

2.3 包组划分与延迟梯度

假设我们发送了两个包组:

包组1: 发送时间 t1_send, 到达时间 t1_recv
包组2: 发送时间 t2_send, 到达时间 t2_recv

延迟梯度 d = (t2_recv - t1_recv) - (t2_send - t1_send)

如果 d > 0,说明第二个包组比第一个包组多花了额外的时间在路上。这个额外时间就是排队延迟。

我在项目中遇到过一个问题:当网络抖动很大时,单个d值波动剧烈,根本没法直接用来做决策。所以GCC不会只看一个d值,而是看一段时间内的趋势。

2.4 趋势检测:过激检测器(Overuse Detector)

GCC使用一个叫过激检测器的模块来判断网络状态。它维护一个延迟趋势的斜率:

  • 斜率 > 阈值 → 延迟在上升 → 判定为过激(Overuse)
  • 斜率接近0 → 延迟稳定 → 判定为正常(Normal)
  • 斜率 < 负阈值 → 延迟在下降 → 判定为欠载(Underuse)

这个阈值很关键。阈值太小,算法太敏感,稍微抖一下就降速;阈值太大,反应迟钝,拥塞了还往上冲。

我的经验:GCC默认阈值是动态调整的。它根据当前网络抖动量自适应地调整阈值。抖动大时阈值放大,避免误判;抖动小时阈值缩小,提高灵敏度。这个设计很实用,我在调优时一般不动这个逻辑,除非遇到极端场景。

2.5 速率控制:AIMD策略

检测到网络状态后,GCC使用AIMD(加性增乘性减)策略调整发送速率:

网络状态 动作 速率变化
欠载(Underuse) 加速 加性增加:+8 kbps/次
正常(Normal) 保持 不变
过激(Overuse) 减速 乘性减少:×0.85

为什么加速是加性,减速是乘性?因为网络拥塞时你需要快速刹车,而网络空闲时可以慢慢试探上限。这个设计理念在TCP拥塞控制里也有体现。

我曾经在调试一个跨国视频会议时,发现GCC的减速系数0.85在某些链路上太激进,导致视频质量频繁波动。后来我们改成了0.9,牺牲了一点响应速度,换来了更稳定的体验。

2.6 整体流程图

下面这张图展示了Delay-based Control的完整流程:

GCC Delay-based Control 流程图 包组划分(Packet Group) 延迟梯度计算(d值) 趋势检测(过激检测器) 状态判断 Overuse 乘性减少 ×0.85 Normal 速率保持不变 Underuse 加性增加 +8kbps 说明:过激检测器根据延迟梯度趋势判断网络状态,然后触发不同的速率调整策略

2.7 卡尔曼滤波器的作用

你可能好奇:趋势斜率是怎么算出来的?GCC早期版本用的是卡尔曼滤波器(Kalman Filter)。

卡尔曼滤波器在这里做的是:从带噪声的延迟观测值中,估计出真实的延迟变化趋势。它维护两个状态:延迟斜率和截距。每次新的包组到达,它就更新这两个估计值。

我记得有一次在WiFi环境下测试,网络抖动特别大,延迟梯度d值在-50ms到+80ms之间乱跳。但卡尔曼滤波器输出的趋势斜率依然稳定,没有误触发过激检测。这就是滤波器的价值。

注意:卡尔曼滤波器需要调参。过程噪声协方差和观测噪声协方差这两个参数直接影响滤波效果。调得太小,滤波器反应慢;调得太大,滤波器跟着噪声跑。我一般建议在实验室环境下先用录制好的网络轨迹跑一遍,找到合适的参数范围。

2.8 与丢包控制的关系

Delay-based Control和Loss-based Control是并行工作的。谁给出的速率低,谁就生效。这叫做“取最小值”策略。

为什么这样设计?因为延迟控制反应快,但可能误判;丢包控制反应慢,但判断更可靠。两者互补:

  • 延迟控制:在拥塞初期就降速,避免丢包
  • 丢包控制:作为兜底,如果延迟控制没拦住,丢包了还能补救

实际项目中,我见过很多场景是延迟控制先降速,然后丢包控制发现没有丢包了,就不再进一步降速。两者配合得很好。

2.9 避坑指南

最后分享几个我在实战中踩过的坑:

  • 时钟不同步问题:发送端和接收端的时钟如果不同步,延迟梯度计算会出错。我曾经遇到过两端时钟偏差200ms的情况,导致GCC完全失效。解决方案是用NTP同步,或者在接收端做时钟补偿。
  • 跨网段路由:当数据包经过不同路径时,延迟梯度会突变。GCC会误判为拥塞。这种情况我建议在应用层加一个“路径变化检测”逻辑,检测到路径变化时重置GCC状态。
  • 低码率场景:当目标码率低于50kbps时,包组划分可能失效,因为包太少。这时候GCC会退化为纯丢包控制。我在做音频通话优化时遇到过这个问题,后来在低码率下直接固定了发送间隔。