13、Pacing与发送平滑:Pacer的作用、漏桶算法、令牌桶算法、GCC与Pacer的配合
各位好,我是你们的WebRTC老司机。今天咱们聊聊Pacer,也就是发送平滑模块。
说实话,我刚开始接触WebRTC的时候,觉得Pacer就是个“限速器”,没啥技术含量。后来被线上问题狠狠教育了一回——那次视频通话卡得跟幻灯片似的,用户投诉电话都打爆了。我排查了半天,发现罪魁祸首就是发送端突发数据太多,把网络缓冲区撑爆了。嗯,从那以后,我再也不敢小看Pacer了。
Pacer到底在解决什么问题?
你想想看,视频编码器出来的数据是“一阵一阵”的。比如一帧画面编码完,突然丢出几十KB甚至几百KB的数据。如果这时候直接往网络上怼,会发生什么?
- 网络缓冲区瞬间被填满,丢包率飙升
- RTT(往返时延)剧烈抖动,拥塞控制算法直接懵圈
- 接收端缓冲区也扛不住,视频播放一卡一卡的
说白了,Pacer的作用就是把“暴饮暴食”变成“少食多餐”。它把编码器吐出来的大块数据,拆成小包,按照一个平稳的速率发送出去。这样网络就不会被突然的流量峰值冲垮。
核心目标:将突发流量整形为平滑的发送流,避免网络拥塞和缓冲区膨胀。
漏桶算法 vs 令牌桶算法
Pacer的实现,说白了就两种经典算法:漏桶和令牌桶。我在项目中两种都折腾过,各有各的脾气。
漏桶算法(Leaky Bucket)
漏桶的思路很简单:想象一个桶,底部有个小洞。数据像水一样倒进去,不管倒得多猛,流出去的速度永远是小洞限定的速率。桶满了,再倒水就溢出了(丢包)。
我个人的习惯是,用漏桶做严格的速率限制。比如你设定发送速率是1Mbps,那漏桶保证平均速率绝对不会超过1Mbps。但它的缺点也很明显——如果桶里没水了,发送速率会瞬间降到0,造成“饥饿”现象。
// 漏桶算法伪代码(简化版)
class LeakyBucket {
double capacity; // 桶容量(字节)
double leakRate; // 漏水速率(字节/秒)
double water; // 当前水量
int64_t lastTime; // 上次漏水时间
bool canSend(int packetSize) {
int64_t now = getTimeMs();
double elapsed = (now - lastTime) / 1000.0;
water = max(0.0, water - elapsed * leakRate); // 先漏水
lastTime = now;
if (water + packetSize <= capacity) {
water += packetSize;
return true; // 可以发送
} else {
return false; // 桶满了,丢弃或排队
}
}
};
避坑指南:我曾经在低码率场景下(比如30kbps)用漏桶,结果发现发送端频繁“断流”。原因是桶容量设得太小,数据稍微积压就溢出了。后来我把容量调大到码率的2倍,问题才解决。
令牌桶算法(Token Bucket)
令牌桶就灵活多了。它不直接限制数据流,而是限制“令牌”的产生速率。每个数据包发送前,必须先拿到足够数量的令牌。令牌以恒定速率生成,但可以累积(最多到桶容量)。
令牌桶的好处是:允许短时间内的突发。比如你平时码率1Mbps,但突然有一帧关键帧数据量很大,只要桶里攒了足够的令牌,就可以一次性发出去。这在视频通话中非常实用——关键帧不能丢,否则画面会花屏好几秒。
// 令牌桶算法伪代码
class TokenBucket {
double maxTokens; // 桶容量(令牌数)
double fillRate; // 令牌生成速率(令牌/秒)
double tokens; // 当前令牌数
int64_t lastTime; // 上次填充时间
bool canSend(int packetSize) {
int64_t now = getTimeMs();
double elapsed = (now - lastTime) / 1000.0;
tokens = min(maxTokens, tokens + elapsed * fillRate); // 先填充令牌
lastTime = now;
if (tokens >= packetSize) {
tokens -= packetSize;
return true; // 有足够令牌,发送
} else {
return false; // 令牌不足,等待
}
}
};
注意:令牌桶的桶容量不能设得太大。否则长时间不发送数据,桶里攒了一堆令牌,突然一股脑全发出去,跟没做Pacing一样。我一般把桶容量设为码率的1~2倍(以字节计)。
GCC与Pacer的配合
GCC(Google Congestion Control)是WebRTC的拥塞控制核心。它根据网络状况动态调整目标码率。但GCC只负责“定目标”,真正执行发送的是Pacer。
它们俩的配合,说白了就是一句话:GCC定速,Pacer执行。
具体流程是这样的:
- GCC的基于延迟的控制器(Delay-based Controller)和基于丢包的控制器(Loss-based Controller)各自估算出一个码率。
- 取两者的最小值,作为最终的目标码率。
- 这个目标码率被喂给Pacer,作为它的发送速率上限。
- Pacer按照这个速率,把编码后的数据包平滑地发送出去。
我在项目中遇到过一个问题:GCC突然把码率从2Mbps降到200kbps,但Pacer的桶里还积压着大量数据。结果Pacer继续以2Mbps的速率发送积压数据,导致网络拥塞加剧。后来我加了一个逻辑:当GCC下调码率时,Pacer必须立即清空或丢弃部分积压数据,否则拥塞控制就是个摆设。
关键配合点:Pacer的发送速率必须实时跟随GCC的码率调整,不能有滞后。否则拥塞控制算法会“误判”网络状况。
Pacer在WebRTC中的实际实现
WebRTC的Pacer实现(位于modules/pacing/目录)用的是令牌桶+优先级队列的组合。它把数据包分成不同的优先级(音频最高,视频次之,FEC和重传包最低),然后按照优先级和令牌桶的约束来发送。
我记得有一次调试一个音频卡顿的问题,发现Pacer把音频包和视频包混在一起发送,导致音频包被视频包“堵”住了。解决方案是:音频包走独立的Pacer队列,并且优先级设为最高。这样即使视频码率再高,音频包也能优先发送。
下面这张图展示了Pacer在WebRTC发送链路中的位置和内部结构:
总结一下
Pacer不是可有可无的“装饰品”,它是WebRTC发送链路中的交通警察。没有它,GCC再聪明也没用——因为突发流量会让网络瞬间恶化,GCC根本来不及反应。
我个人建议,如果你在调优WebRTC的发送性能,先检查Pacer的配置:
- 令牌桶容量是否合理?
- 优先级队列是否区分了音频和视频?
- Pacer的发送速率是否实时跟随GCC的调整?
这三个点搞定,你的视频通话流畅度至少提升一个档次。
最后一个小技巧:在WebRTC的日志中搜索“Pacer”,看看有没有“Overuse”或“Underuse”的警告。如果有,说明Pacer和GCC的配合出了问题,需要调整参数。