3. GCC算法深度解析:基于丢包的拥塞控制(Loss-based Control)的工作原理
GCC(Google Congestion Control)是WebRTC的流量控制核心。很多人一听到拥塞控制就想到BBR、CUBIC这些大名鼎鼎的算法,但GCC其实走了一条完全不同的路。它不依赖传统的丢包重传窗口,而是把丢包率和延迟梯度两个信号分开处理。
这一章我们先啃丢包部分。说白了,就是当网络开始丢包时,GCC怎么判断该不该降码率,降多少。
3.1 丢包率计算的底层逻辑
GCC的丢包检测不依赖RTCP的Sender Report,它用的是Transport-cc反馈。每个RTP包头上都带了一个Transport Sequence Number,接收端收到后,会定期发回Transport-cc Feedback报文,告诉发送端:哪些包到了,哪些没到。
我刚开始看这个机制时,觉得不就是个ACK吗?后来踩了坑才发现,Transport-cc的妙处在于它不依赖RTT。你想想看,传统TCP的丢包判断要等超时,但WebRTC是实时通信,等不起。
核心公式:
丢包率 = (已发送包数 - 已到达包数) / 已发送包数
GCC每200ms计算一次,窗口大小固定为500ms内的包。
这里有个细节:GCC只统计已发送超过1秒的包。为什么?因为刚发出去的包可能还在路上,如果立刻算进去,丢包率会虚高。我记得有一次线上排查,发现某条链路的丢包率一直在5%左右波动,但用户反馈卡顿严重。后来查出来是Transport-cc反馈报文被运营商限速了,导致大量包被误判为丢失。嗯,这个坑后面会细讲。
3.2 丢包率阈值与状态机
GCC的丢包控制不是线性的,它维护了一个三状态机:
| 状态 | 丢包率范围 | 行为 |
|---|---|---|
| Increase | 丢包率 < 2% | 尝试增加码率 |
| Hold | 2% ≤ 丢包率 < 10% | 保持当前码率 |
| Decrease | 丢包率 ≥ 10% | 降低码率 |
你可能会问:为什么2%以下就敢加码率?因为WebRTC的丢包检测粒度很细,2%的丢包率在实时通信中其实算正常。我曾经在Wi-Fi环境下做过测试,即使信号满格,偶尔也会出现0.5%~1%的随机丢包。如果一丢包就降码率,那视频质量会像过山车一样。
但10%这个阈值就很有意思了。GCC认为,一旦丢包率超过10%,说明网络已经严重拥塞,必须立刻降码率。降多少?每次降低当前码率的(1 - 0.88)倍,也就是砍掉12%。
个人经验: 我在调优时发现,10%阈值对有线网络很合适,但移动网络(4G/5G)的丢包模式不同。移动网络经常出现突发丢包,瞬间丢包率可能冲到30%,但下一秒又恢复正常。如果严格按照GCC的10%阈值,码率会剧烈抖动。我后来在移动场景下把阈值放宽到15%,配合延迟基控制,效果好了很多。
3.3 码率调整的数学细节
GCC的码率调整公式其实不复杂,但很多人被代码里的lambda表达式绕晕了。我们直接看核心逻辑:
// 伪代码:GCC丢包控制核心
if (loss_rate < 0.02) {
// Increase: 加性增
bitrate = bitrate * 1.08;
} else if (loss_rate < 0.10) {
// Hold: 保持
// 什么都不做
} else {
// Decrease: 乘性减
bitrate = bitrate * 0.88;
}
注意看Increase阶段,用的是乘性增(乘以1.08),而不是TCP的加性增。为什么?因为实时通信需要快速抢占带宽。你想想看,如果慢慢加,等码率加上去,用户可能已经卡了10秒了。
但乘性增也有问题:它太激进了。如果当前码率是1Mbps,乘以1.08就是1.08Mbps,一次增加80kbps。但如果当前码率是5Mbps,一次增加400kbps。这会导致高码率场景下,码率增长过快,容易把网络撑爆。
避坑指南: 我曾经在一个项目中,把乘性增系数从1.08改成了1.05,想让它更平滑。结果发现,在带宽充足但RTT较高的链路上,码率增长太慢,用户看到的视频一直是模糊的。后来我改成动态系数:当丢包率低于1%时用1.08,1%~2%之间用1.04。这样既保证了快速抢占,又避免了过度激进。
3.4 丢包控制的局限性
丢包控制虽然简单有效,但它有个致命缺陷:滞后性。只有当丢包已经发生时,它才会做出反应。对于实时通信来说,这太晚了。
举个例子:你正在看一个直播,网络突然变差。GCC的丢包控制流程是这样的:
- 发送端发出包A、B、C、D、E
- 接收端只收到A、B、C,没收到D、E
- 接收端发回Transport-cc反馈
- 发送端收到反馈,计算丢包率
- 丢包率超过10%,触发降码率
从丢包发生到降码率生效,至少需要一个RTT + 200ms。如果RTT是100ms,那就是300ms的延迟。这300ms里,用户已经感受到了卡顿。
所以GCC引入了第二个武器:延迟基控制(Delay-based Control)。它通过检测延迟的微小变化,在丢包发生之前就预判拥塞。这个我们下一章会详细讲。
3.5 丢包控制与FEC的配合
丢包控制不是孤立的。在实际系统中,它和FEC(前向纠错)紧密配合。当丢包率较低时(比如2%~5%),GCC会倾向于用FEC来恢复丢失的包,而不是降码率。只有当丢包率超过FEC的恢复能力时,才会触发降码率。
我见过一个常见的配置错误:有人把FEC冗余度设得特别高,结果有效码率反而下降了。比如原始码率1Mbps,FEC冗余设了50%,实际发送码率变成1.5Mbps。如果网络带宽只有1.2Mbps,反而会触发丢包,然后GCC降码率,陷入恶性循环。
最佳实践:
- 丢包率 < 2%:不启用FEC,纯丢包控制
- 丢包率 2%~5%:启用FEC,冗余度20%~30%
- 丢包率 5%~10%:FEC冗余度提高到50%,同时准备降码率
- 丢包率 > 10%:降码率为主,FEC为辅
3.6 整体流程可视化
下面这张图展示了GCC丢包控制的完整流程。从Transport-cc反馈的接收,到丢包率计算,再到状态机决策和码率调整,每一步都有明确的触发条件。
3.7 调试与监控要点
在实际调试中,我建议重点关注以下几个指标:
- 丢包率曲线:看它是平稳上升还是突发跳变。平稳上升说明网络在缓慢恶化,突发跳变可能是路由器缓存溢出。
- 码率调整频率:如果码率在1分钟内调整超过5次,说明网络不稳定,可能需要调整阈值。
- Transport-cc反馈延迟:如果反馈报文延迟超过200ms,丢包率计算会失真。
调试技巧: 在Chrome的webrtc-internals页面,找到"googLossBasedBwe"这个字段。它显示的是GCC丢包控制计算出的目标码率。如果这个值频繁在Increase和Decrease之间切换,说明你的网络正在经历"拥塞抖动"。这时候不要急着改算法,先看看是不是Wi-Fi信号不稳定。