8、基于丢包的拥塞控制(Loss-based):丢包率计算、RTCP RR报文解析、丢包阈值设计、与延迟控制器的协作
聊到基于丢包的拥塞控制,我得先说说我的一个心路历程。刚接触WebRTC那会儿,我总觉得丢包检测这事儿挺简单的——数数包没了不就行了?后来在项目中踩了坑才发现,这里面的门道比想象中深得多。你想想看,网络环境那么复杂,偶尔丢一两个包到底是拥塞造成的,还是单纯的线路抖动?这问题要是搞不清楚,带宽估计就会像过山车一样忽高忽低。
这一章,我们就来把基于丢包的拥塞控制彻底拆开揉碎。我会从最底层的RTCP RR报文讲起,再到丢包率的精确计算、阈值设计的艺术,最后聊聊它怎么跟延迟控制器配合工作。嗯,这些都是我在实际项目中反复调优过的经验,希望能帮你少走弯路。
8.1 RTCP RR报文:拥塞控制的数据基石
基于丢包的拥塞控制,第一步就是拿到准确的丢包数据。这个数据从哪来?RTCP RR报文。
RTCP RR,全称是Receiver Report。接收端每隔一段时间就会给发送端发一个RR报文,里面装着接收质量的关键指标。我个人习惯把RR报文看作是网络的「体检报告」——它告诉你网络当前的健康状况。
RR报文的核心字段如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| SSRC of sender | 32 bit | 发送端的同步源标识 |
| SSRC of source | 32 bit | 接收端报告的源标识 |
| Fraction lost | 8 bit | 上一个报告周期内的丢包率(小数部分) |
| Cumulative lost | 24 bit | 从开始到现在的累计丢包数 |
| Extended highest sequence number | 32 bit | 收到的最高序列号(含扩展位) |
| Interarrival jitter | 32 bit | 到达时间抖动 |
| Last SR timestamp | 32 bit | 最近一次发送SR的时间戳 |
| Delay since last SR | 32 bit | 从上次SR到现在的延迟 |
这里最关键的字段就是 Fraction lost 和 Cumulative lost。Fraction lost 是8位定点小数,取值范围0到255,对应丢包率0%到100%。比如值是25,就代表丢包率大约是9.8%(25/255)。
我在项目中遇到过一个问题:有些接收端实现的RR报文里,Fraction lost字段总是0,但Cumulative lost却在增长。后来排查发现,是接收端把Fraction lost的计算周期搞错了。所以我的建议是:不要完全信任Fraction lost,最好自己根据Cumulative lost和序列号差值重新算一遍丢包率。
8.2 丢包率计算:从原始数据到可用指标
拿到RR报文后,怎么算出真正可用的丢包率?这里有几个关键点。
第一,计算周期要对齐。 RR报文不是每个包都发,而是每隔一段时间发一次。WebRTC中默认的RTCP间隔是5秒。你算丢包率的时候,必须用当前RR和上一个RR之间的差值,而不是从0开始算。
计算公式很简单:
// 伪代码示例
uint32_t lost_since_last = current_cumulative_lost - last_cumulative_lost;
uint32_t expected_since_last = (current_extended_max_seq - last_extended_max_seq);
float loss_rate = (float)lost_since_last / (float)expected_since_last;
第二,序列号回绕问题。 RTP序列号是16位的,范围0到65535。超过65535后会回绕到0。如果你不做处理,算出来的丢包率会变成负数或者巨大的正数。
我曾经在一个跨国项目中遇到过这个问题。当时视频通话突然卡顿,查了半天发现是序列号回绕导致丢包率计算错误,触发了带宽骤降。解决方案是使用扩展序列号——在16位序列号前面加一个扩展位,变成32位,这样就不会回绕了。
核心要点: 丢包率计算必须基于扩展序列号,并且使用差分方式计算,避免累积误差和回绕问题。
第三,平滑处理。 单次计算的丢包率抖动很大,直接用它来做决策会导致带宽频繁变化。我一般会用一个指数移动平均(EMA)来做平滑:
// 平滑丢包率计算
const float kAlpha = 0.1f; // 平滑因子,值越小越平滑
smoothed_loss_rate = kAlpha * current_loss_rate + (1 - kAlpha) * smoothed_loss_rate;
平滑因子kAlpha怎么选?我个人的经验是:网络稳定的环境下用0.05,网络抖动大的环境下用0.15。前者更平滑但反应慢,后者反应快但容易误判。没有完美的值,只有适合你场景的值。
8.3 丢包阈值设计:什么时候该降速?
丢包率算出来了,接下来就是决策问题:丢包率到多少的时候,我应该降低发送速率?
WebRTC中常用的阈值设计是这样的:
| 丢包率范围 | 行为 | 说明 |
|---|---|---|
| 0% - 2% | 保持当前速率 | 正常网络波动,无需处理 |
| 2% - 10% | 适度降低速率 | 按比例降速,通常降5%-10% |
| 10% - 20% | 大幅降低速率 | 降速20%-30%,进入保守模式 |
| > 20% | 紧急降速 | 降速50%以上,甚至暂停发送 |
这些阈值不是拍脑袋定的。我记得在Google的WebRTC源码中,默认的丢包阈值是2%和10%。为什么是2%?因为实验表明,在大多数有线网络中,正常情况下的丢包率都低于1%。2%已经是一个明显的拥塞信号了。
但这里有个坑:无线网络(Wi-Fi、4G/5G)的丢包率本身就比较高,经常在1%-3%之间波动。如果你用2%作为阈值,在无线网络下就会频繁触发降速,导致用户体验很差。
避坑指南: 我曾经在一个移动端项目中,用户反馈在4G网络下视频经常卡顿。排查后发现是丢包阈值设置得太低,导致频繁降速。后来我们把阈值从2%调整到5%,并增加了延迟判断——只有丢包率超过5%且延迟也升高时,才触发降速。效果好了很多。
所以我的建议是:不要死守固定阈值。可以根据网络类型动态调整阈值。比如检测到是Wi-Fi网络,用2%阈值;检测到是蜂窝网络,用5%阈值。或者更高级的做法:根据历史数据自适应调整阈值。
8.4 与延迟控制器的协作:双剑合璧
基于丢包的拥塞控制有个天然缺陷:它反应太慢了。等你检测到丢包的时候,网络其实已经拥塞了一段时间了。这就像你开车看到前方堵车了才踩刹车,已经晚了。
所以WebRTC中,基于丢包的控制器通常和基于延迟的控制器(比如GCC中的延迟控制器)配合使用。它们的关系是这样的:
从上面的流程图可以看出,两个控制器是并行工作的。但它们的优先级不同:
- 延迟控制器优先:延迟变化能更快地反映网络状态,所以它先做决策。
- 丢包控制器作为后备:当延迟控制器无法判断时(比如网络突然拥塞导致大量丢包),丢包控制器介入。
具体协作逻辑是这样的:
- 延迟控制器持续监控延迟梯度。如果延迟持续上升,说明网络开始拥塞,它会先降低发送速率。
- 如果延迟控制器已经降速了,但丢包率仍然在上升,说明拥塞很严重,丢包控制器会进一步降速。
- 如果延迟控制器判断网络恢复正常(延迟下降),它会尝试提升速率。但丢包控制器会检查丢包率是否也下降了——如果丢包率还很高,它会阻止速率提升。
说白了,延迟控制器负责「快反应」,丢包控制器负责「稳判断」。两者配合,才能既快速响应网络变化,又避免误判。
个人经验: 我在调优这两个控制器的协作时,发现一个关键参数——两个控制器输出带宽估计值的「融合权重」。延迟控制器的权重应该高一些(比如0.7),丢包控制器的权重低一些(0.3)。这样既能保证快速响应,又能防止丢包抖动带来的误调整。
8.5 实际项目中的避坑指南
最后,分享几个我在实际项目中踩过的坑,希望能帮你避开:
- 坑一:RTCP RR报文延迟。RR报文不是实时发送的,它有自己的发送间隔。如果你在RR报文到达之前就做决策,可能会基于过时的数据。我的做法是:缓存最近两次RR报文,只有当新的RR到达时才更新丢包率。
- 坑二:丢包率与重传包的混淆。有些场景下,接收端会请求重传丢失的包。重传成功后,丢包率应该下降。但如果你计算丢包率时没有排除重传包,就会导致丢包率虚高。我建议在计算丢包率时,只统计原始发送的包,不统计重传包。
- 坑三:阈值滞后设计。我曾经发现,当丢包率在阈值附近来回波动时,带宽会频繁变化。解决方案是引入滞后(hysteresis):比如降速阈值是2%,但恢复阈值设为1.5%。这样丢包率在1.5%-2%之间波动时,不会触发频繁调整。
嗯,关于基于丢包的拥塞控制,核心内容就是这些了。记住一句话:丢包是结果,不是原因。真正要解决的是导致丢包的拥塞问题,而不是单纯地应对丢包本身。理解了这一点,你就能更好地设计你的拥塞控制算法了。
公众号:蓝海资料掘金营,微信deep3321