WebRTC 全景实战 (12):SFU/MCU/Mesh 架构与 Pion 实战
"我们早期押注多播,但公网教会了我们:你需要的是 packet shufflers,不是 multicast routers。" — Serge Lachapelle,Curious 历史访谈
本系列从 P2P(Ch2)走来,现在进入多人会议的架构选型。Marratech 是瑞典最早的 Web 视频会议公司之一,2009 年被 Google 收购——Serge Lachapelle 随之加入 Google,成为 WebRTC 标准化的核心推动者。他在 Curious 访谈中回忆:Marratech 早期押注 IP 多播,后来行业转向 packet shufflers——即今天的 SFU(Selective Forwarding Unit)。
配套 Lab:examples/webrtc-lab/client/ch12-sfu-client(LiveKit 客户端)+ examples/webrtc-lab/signaling/。
本章涉及 LiveKit 作为 SFU 参考实现。建议先阅读本站 LiveKit 介绍,了解 Room/Participant/Track 模型与 SDK 选型。
本篇术语表
| 术语 | 英文 | 解释 |
|---|---|---|
| Mesh | 网状拓扑 | 每个参与者与其他所有人建立 P2P 连接 |
| MCU | Multipoint Control Unit | 多点控制单元,服务端混流/转码后广播 |
| SFU | Selective Forwarding Unit | 选择性转发单元,服务端按订阅转发 RTP 包 |
| Forwarder | 转发器 | SFU 核心模块,接收 RTP 并按需转发给订阅者 |
| Room | 房间 | 逻辑会话容器,包含多个 Participant |
| Participant | 参与者 | Room 内的一个连接实体(人、Agent、录制器) |
| Track | 轨道 | 单条媒体流(音频/视频/屏幕共享) |
| Publication | 发布 | Track 的发布状态与元数据 |
| Subscription | 订阅 | 订阅者与远程 Track 的绑定关系 |
| Pion | — | Go 语言 WebRTC 库,Curious 作者 Sean-Der 主导 |
| StreamTracker | 流追踪器 | SFU 内部跟踪每个 Track 各层可用性的组件 |
| LayerSelector | 层选择器 | 根据带宽为订阅者选择 Simulcast/SVC 层 |
| Control Plane | 控制面 | 信令、Room 管理、Token 鉴权 |
| Media Plane | 媒体面 | SRTP 媒体流转发,高带宽低延迟 |
| Webhook | 回调 | Room 事件通知后端(join/leave/publish) |
| Node | 节点 | 分布式 SFU 集群中的单台物理/虚拟服务器 |
一、三种架构对比
| 架构 | 延迟 | 服务端 CPU | 客户端 CPU | 客户端带宽 | 规模 | 典型产品 |
|---|---|---|---|---|---|---|
| Mesh | 最低 | 无 | O(N²) 解码 | O(N) 上下行 | ≤4 人 | 1v1 通话 |
| MCU | 较高(混流) | 极高(转码) | 低(1 路) | 低(1 路) | 传统会议 | 早期 Zoom |
| SFU | 低 | 低(转发) | O(N) 解码 | O(N) 下行 | 10–10k+ | Meet/Teams/LiveKit |
1.1 连接数增长对比
Mesh 在 N=6 时需要 15 条 P2P 连接,N=10 时需要 45 条——客户端和网络都无法承受。SFU 将连接数降为 O(N)。
1.2 何时仍用 Mesh?
| 场景 | 原因 |
|---|---|
| 1v1 通话 | 无 SFU 开销,延迟最低 |
| 2–3 人小群 | 连接数可控 |
| 端到端加密 E2EE | 无服务端转发(SFU 需特殊 E2EE 方案) |
| 超低延迟游戏 | 避免 SFU 额外一跳 |
二、历史脉络:Marratech → Meet → LiveKit
Serge Lachapelle 在 Curious 访谈中强调:WebRTC 标准化的成功在于站在巨人肩膀上——RTP、SRTP、ICE 等协议早已成熟,WebRTC 做的是把它们整合成浏览器 API。SFU 架构同理:不是发明新的媒体协议,而是在正确的位置做 RTP 包调度。
Marratech 被收购后,其核心工程师将 packet shuffler 经验带入 Google Meet。Sean-Der(Pion 作者)则在 Curious 一书中系统化了 SFU 的实现细节——LiveKit 正是 Pion 之上构建的生产级 SFU。
三、SFU 核心数据模型
LiveKit 的 Room / Participant / Track 模型是 SFU 领域的标准抽象:
| 概念 | 说明 | 示例 |
|---|---|---|
| Room | 会话容器,有唯一 ID | meeting-123 |
| Participant | Room 内的连接实体 | alice, bob |
| Track | 单条媒体流 | TR_abc123 (video), TR_def456 (audio) |
| Publication | Track 的发布状态 | muted, simulcast, source |
| Subscription | 订阅关系 | P2 订阅 P1 的 video Track |
详见 LiveKit 介绍——该文详细对比了 LiveKit 与 mediasoup/Janus 的选型,以及 SDK 集成路径。
3.1 Track 生命周期
3.2 JWT Access Token 结构
import { AccessToken } from "livekit-server-sdk";
const token = new AccessToken(apiKey, apiSecret, { identity: "alice" });
token.addGrant({
roomJoin: true,
room: "meeting-123",
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
const jwt = await token.toJwt();
四、SFU Forwarder 内部逻辑
Pion(Go)和 LiveKit(Go + Pion)都实现了这套 Forwarder + StreamTracker 管线。关键设计原则:
- 不转码:只修改 RTP 头,payload 原样转发
- per-subscriber 状态:每个订阅者独立的 SSRC/seq/timestamp 空间
- per-subscriber BWE:每个订阅者独立的 TWCC 和 LayerSelector
- Simulcast 层选择:根据 Ch11 的 rid/DD 选层
4.1 RTP 头重写
SFU 必须为每个订阅者维护独立的 RTP 序列号空间,否则订阅者的 jitter buffer 和 TWCC 会混乱。
4.2 StreamTracker 职责
| 检测项 | 作用 |
|---|---|
| 各 rid 层是否有 Keyframe | 层切换前提 |
| 各层最后活跃时间 | Dynacast 停发依据 |
| MID/SSRC 映射变化 | Re-negotiation 处理 |
| DD 空间/时间层可用性 | SVC 选层输入 |
五、开源 SFU 选型
| 项目 | 语言 | 定位 | 特点 |
|---|---|---|---|
| Pion | Go | 库 | WebRTC for the Curious 作者 Sean-Der 主导,最大灵活度 |
| mediasoup | C++/Node | 框架 | 灵活,需自建信令/Room/鉴权 |
| Janus | C | 框架 | 插件化,SIP 集成强 |
| LiveKit | Go | 产品 | 完整栈:SFU + SDK + Agents + Egress |
| Jitsi Videobridge | Java | 产品 | Jitsi Meet 后端,成熟但 Java 栈 |
LiveKit 介绍 从控制面/媒体面分离、SDK 生态、Agents 扩展等维度做了更完整的选型分析——本章聚焦 SFU 媒体面原理,LiveKit 作为贯穿 Ch12–Ch15 的参考实现。
5.1 选型决策树
5.2 mediasoup vs LiveKit 模型对比
| 概念 | mediasoup | LiveKit |
|---|---|---|
| 路由单元 | Router | Room |
| 传输通道 | WebRtcTransport | Participant PC |
| 媒体流 | Producer / Consumer | Track Publication / Subscription |
| 信令 | 自建 | 内置 WebSocket |
| 鉴权 | 自建 | JWT Token |
六、Pion 最小 SFU 概念代码
// 概念示意 — 非完整可运行代码
// 完整示例见 github.com/pion/example-webrtc-applications
package main
import (
"github.com/pion/webrtc/v4"
)
func main() {
api := webrtc.NewAPI()
publisherPC, _ := api.NewPeerConnection(webrtc.Configuration{})
subscribers := make(map[string]*webrtc.PeerConnection)
publisherPC.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
for _, subPC := range subscribers {
localTrack, _ := webrtc.NewTrackLocalStaticRTP(
track.Codec().RTPCodecCapability,
track.ID(),
track.StreamID(),
)
subPC.AddTrack(localTrack)
go func(t *webrtc.TrackRemote, lt *webrtc.TrackLocalStaticRTP) {
buf := make([]byte, 1500)
for {
n, _, err := t.Read(buf)
if err != nil { return }
lt.Write(buf[:n])
}
}(track, localTrack)
}
})
}
上述代码缺少 Simulcast 层选择、TWCC、SRTP 密钥管理、Room 状态、断线重连等。这就是为什么大多数团队选择 LiveKit 而非从零用 Pion 构建——详见 LiveKit 介绍。
七、实战:LiveKit 三人会议
7.1 启动 LiveKit Server
brew install livekit
livekit-server --dev
# 输出: LiveKit server started on :7880
7.2 生成 Access Token
brew install livekit-cli
lk token create \
--api-key devkey \
--api-secret secret \
--join \
--room meeting \
--identity user1 \
--valid-for 24h
7.3 客户端连接
// examples/webrtc-lab/client/ch12-sfu-client
import { Room, RoomEvent, Track } from "livekit-client";
const room = new Room();
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
if (track.kind === Track.Kind.Video) {
const element = track.attach();
document.getElementById("remote-videos").appendChild(element);
}
});
room.on(RoomEvent.ParticipantConnected, (p) => {
console.log("[Layer1] joined:", p.identity);
});
await room.connect("ws://localhost:7880", token);
await room.localParticipant.enableCameraAndMicrophone();
console.log("Participants:", room.numParticipants);
React 组件方式:
import { LiveKitRoom, VideoConference } from "@livekit/components-react";
function Meeting() {
return (
<LiveKitRoom token={token} serverUrl="ws://localhost:7880">
<VideoConference />
</LiveKitRoom>
);
}
7.4 验证三人会议
八、分布式 SFU Mesh
LiveKit 默认启用分布式 Mesh——单个 Room 可跨多台物理服务器:
| 组件 | 作用 |
|---|---|
| Redis | Room 路由表、Node 注册、Participant 位置 |
| Node Selector | 新 Participant 分配到最优 Node |
| Relay | 跨 Node 的 RTP 转发(当发布者和订阅者在不同 Node) |
跨 Node Relay 会增加一跳延迟——生产环境应通过 GeoDNS + 区域亲和性 让同一 Room 的参与者尽量在同一 Node(Ch14 详述)。
8.1 Webhook 事件
// 后端接收 LiveKit Webhook
app.post("/webhook/livekit", (req, res) => {
const event = req.body;
switch (event.event) {
case "participant_joined":
console.log(JSON.stringify({ layer: "signaling", event, identity: event.participant.identity }));
break;
case "track_published":
console.log(JSON.stringify({ layer: "media", track: event.track }));
break;
case "room_finished":
// 清理资源
break;
}
res.sendStatus(200);
});
九、SFU vs 信令的关系
关键认知:
- 信令不承载媒体——SDP/ICE 走 WebSocket,SRTP 走 UDP 直连 SFU
- LiveKit 内置信令——
room.connect()同时完成信令 + 媒体协商 - 自研信令 + SFU——mediasoup/Pion 场景需自行实现 Room 管理(Ch3)
examples/webrtc-lab/signaling/server.js 实现了最小 P2P 信令;SFU 场景下信令由 LiveKit 接管,但 Join API 仍可在同一 Node 服务中提供 Token。
十、常见陷阱
| # | 陷阱 | 现象 | 修复 |
|---|---|---|---|
| 1 | Mesh 用于大会议 | 客户端 CPU/带宽爆炸 | N>4 切换 SFU |
| 2 | SFU 做转码 | 延迟高、CPU 爆 | SFU 只转发,混流用 MCU/Egress |
| 3 | 忽略 per-subscriber SSRC | 订阅者 jitter buffer 乱 | Forwarder 重写 SSRC/seq |
| 4 | 单 Node 部署 | 跨区延迟高 | 分布式 Mesh + GeoDNS |
| 5 | Token 无过期 | 安全风险 | JWT 短期 Token + 刷新 |
| 6 | 未启用 Simulcast | 所有订阅者同质量 | 发布端开启 Simulcast |
| 7 | 信令与媒体混淆 | 带宽打满信令服务器 | 媒体必须直连 SFU |
| 8 | 跨 Node Relay 未监控 | 延迟飙升 | Prometheus 监控 relay 延迟 |
| 9 | Webhook 未验签 | 伪造 join 事件 | 验证 LiveKit Webhook 签名 |
| 10 | Room 无空房间清理 | 内存泄漏 | emptyTimeout 配置 |
十一、实战 Lab
Lab 1:LiveKit 三人会议
livekit-server --dev
lk token create --api-key devkey --api-secret secret \
--join --room lab --identity user1 --valid-for 1h
- 3 个浏览器 Tab 加入
room=lab - 验证互相看到视频
chrome://webrtc-internals确认仅 1 条 PeerConnection 到 SFU
Lab 2:Pion 示例运行
git clone https://github.com/pion/example-webrtc-applications
cd example-webrtc-applications/sfu-ws
go run main.go
# 浏览器打开 http://localhost:8080
Lab 3:Simulcast 层观察
- LiveKit 三人会议中,一端 DevTools 限速 300kbps
- 在 LiveKit Dashboard 观察该订阅者的下行码率
- 确认 LayerSelector 切换到
rid=l
Lab 4:Participant 事件
room.on(RoomEvent.ParticipantConnected, (p) => console.log("joined:", p.identity));
room.on(RoomEvent.ParticipantDisconnected, (p) => console.log("left:", p.identity));
room.on(RoomEvent.TrackMuted, (pub, p) => console.log("muted:", p.identity));
Lab 5:对比 Mesh vs SFU 连接数
- 修改
examples/webrtc-lab/client/ch02-p2p-basic为 3 人 Mesh chrome://webrtc-internals数 PeerConnection 数量(应为 2)- 切换 LiveKit SFU(应为 1)
Lab 6:Token 权限最小化
- 生成
canPublish: false的 Token - 尝试
enableCameraAndMicrophone()→ 应失败 - 验证仅观看模式
Lab 7:Webhook 接收
- 配置 LiveKit
webhook.urls - 加入/离开 Room,观察后端日志
十二、本章小结
| 概念 | 要点 |
|---|---|
| Mesh | 无服务器,O(N²) 连接,≤4 人 |
| MCU | 混流转码,低客户端开销,高服务端 CPU |
| SFU | 选择性转发,不转码,10k+ 规模 |
| Forwarder | RTP 头重写 + LayerSelector + per-subscriber BWE |
| LiveKit | Room/Participant/Track 标准模型,推荐入门 SFU |
| 选型 | 快速上线 LiveKit,深度定制 Pion/mediasoup |
| 历史 | Marratech → packet shuffler → Meet → LiveKit |
下一篇(Ch13):调试工具链与可观测性
系列导航
章节 主题 状态 0 架构全景与协议栈地图 ✅ 已发布 1 浏览器媒体 API 与设备管理 ✅ 已发布 2 第一个 P2P 视频通话 ✅ 已发布 3 信令服务器设计与会话状态机 ✅ 已发布 4 SDP 解剖与媒体协商 ✅ 已发布 5 ICE、STUN、TURN 与 NAT 穿透 ✅ 已发布 6 Data Channel 与 SCTP over DTLS ✅ 已发布 7 DTLS 握手与 SRTP 加密体系 ✅ 已发布 8 RTP/RTCP 媒体传输与 QoS ✅ 已发布 9 音视频编解码与 Simulcast 入门 ✅ 已发布 10 带宽估计与拥塞控制 GCC ✅ 已发布 11 Simulcast、SVC 与选择性订阅 ✅ 已发布 12 SFU/MCU/Mesh 架构与 Pion 实战 ✅ 已发布 13 调试工具链与可观测性 ✅ 已发布 14 TURN 集群部署与多区域扩展 ✅ 已发布 15 Capstone 生产级视频会议系统 ✅ 已发布