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大致分三步走:
- 包组(Packet Group)划分:将连续发送的包分成组,计算每组的一路延迟变化
- 延迟梯度计算:用到达时间差减去发送时间差,得到延迟变化量
- 趋势检测:用卡尔曼滤波器或趋势线判断延迟是在上升还是下降
嗯,这里要注意:包组不是随便分的。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的完整流程:
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会退化为纯丢包控制。我在做音频通话优化时遇到过这个问题,后来在低码率下直接固定了发送间隔。