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的丢包控制流程是这样的:

  1. 发送端发出包A、B、C、D、E
  2. 接收端只收到A、B、C,没收到D、E
  3. 接收端发回Transport-cc反馈
  4. 发送端收到反馈,计算丢包率
  5. 丢包率超过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反馈的接收,到丢包率计算,再到状态机决策和码率调整,每一步都有明确的触发条件。

GCC丢包控制流程图 接收Transport-cc反馈 计算丢包率(500ms窗口) 状态机判断丢包率 Increase(<2%) Hold(2%~10%) Decrease(≥10%) 更新目标码率 说明:每200ms执行一次,窗口滑动500ms内的包

3.7 调试与监控要点

在实际调试中,我建议重点关注以下几个指标:

  • 丢包率曲线:看它是平稳上升还是突发跳变。平稳上升说明网络在缓慢恶化,突发跳变可能是路由器缓存溢出。
  • 码率调整频率:如果码率在1分钟内调整超过5次,说明网络不稳定,可能需要调整阈值。
  • Transport-cc反馈延迟:如果反馈报文延迟超过200ms,丢包率计算会失真。

调试技巧: 在Chrome的webrtc-internals页面,找到"googLossBasedBwe"这个字段。它显示的是GCC丢包控制计算出的目标码率。如果这个值频繁在Increase和Decrease之间切换,说明你的网络正在经历"拥塞抖动"。这时候不要急着改算法,先看看是不是Wi-Fi信号不稳定。