生产环境部署:负载均衡、监控告警、大规模并发优化

说实话,很多团队做 WebRTC 的时候,Demo 跑得飞起,一上生产就崩。我见过最夸张的一次,线上 200 路并发,SFU 直接 OOM,整个服务挂了半小时。嗯,今天我们就聊聊怎么把 WebRTC 服务真正扛住生产压力。

一、架构层面的负载均衡

WebRTC 的负载均衡跟普通 HTTP 不一样。你想想看,HTTP 请求是短连接,做完就完事。但 WebRTC 是长连接,一个用户可能连着好几个小时。所以负载均衡的策略要专门设计。

1. 信令层的负载均衡

信令层说白了就是 WebSocket 或者 HTTP 请求。这部分用 Nginx 做反向代理就行,简单粗暴。我个人习惯用 ip_hash 策略,保证同一个用户的信令请求落到同一台机器上。

upstream signaling_servers {
    ip_hash;
    server 10.0.1.1:8080 weight=5;
    server 10.0.1.2:8080 weight=5;
    server 10.0.1.3:8080 weight=3;
    keepalive 64;
}

server {
    listen 443 ssl;
    location /ws {
        proxy_pass http://signaling_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
我的经验:keepalive 一定要开。我曾经遇到过信令服务器连接数打满,排查了半天才发现是 keepalive 没配,每次握手都新建 TCP 连接。

2. 媒体层的负载均衡

媒体层才是大头。WebRTC 的媒体流走的是 UDP,Nginx 管不了。这里我推荐两种方案:

  • DNS SRV 记录:客户端通过 DNS 查询获取可用的 SFU 节点列表,然后自己选一个连。优点是简单,缺点是客户端要改代码。
  • 专用调度器:部署一个独立的调度服务,负责分配 SFU 节点。调度器根据各节点的 CPU、内存、连接数来做决策。

我在项目中用的是第二种。调度器会维护一个节点状态表,每 5 秒更新一次。

// 调度器核心逻辑(伪代码)
function assignSFU(userId, roomId) {
    let nodes = getAliveNodes();
    // 优先选择同一房间用户所在的节点
    let roomNode = findNodeByRoom(roomId);
    if (roomNode && roomNode.load < 0.8) {
        return roomNode;
    }
    // 否则选负载最低的节点
    nodes.sort((a, b) => a.load - b.load);
    return nodes[0];
}
注意:同一个房间的用户尽量分配到同一个 SFU 节点上。否则跨节点转发会引入额外的延迟和带宽消耗。我曾经踩过这个坑,一个 50 人的大会议室,因为分散到 3 个节点,延迟直接飙到 500ms。

二、监控告警体系

没有监控的生产环境,就像闭着眼睛开车。我个人习惯把监控分成三个层次:

1. 基础设施监控

指标 告警阈值 说明
CPU 使用率 > 80% 持续 5 分钟 编码解码很吃 CPU
内存使用率 > 85% 每个 PeerConnection 约 50-100MB
网络带宽 > 70% 链路带宽 注意出站带宽,SFU 转发会放大流量
UDP 丢包率 > 5% 直接影响音视频质量

2. 业务层监控

业务指标才是真正反映用户体验的。我建议至少采集这些:

  • 并发连接数:当前活跃的 PeerConnection 数量
  • 房间数:同时进行的会议数量
  • 建连成功率:ICE 连接成功的比例,低于 95% 要报警
  • 平均建连时间:从发起呼叫到媒体流到达的时间,超过 5 秒算异常
  • 音视频码率:实时码率如果低于预期,说明网络有问题

核心指标:我个人最看重的是「建连成功率」和「平均建连时间」。这两个指标直接反映了用户能不能正常通话。有一次线上告警,建连成功率掉到 60%,排查发现是 STUN 服务器挂了。

3. 告警策略

告警不是越多越好。我曾经见过一个团队,一天收到 2000 条告警,最后没人看了。我的原则是:

  1. 分级告警:P0 级(服务不可用)直接电话+短信,P1 级(指标异常)发企业微信,P2 级(趋势预警)发邮件
  2. 抑制重复:同一个告警 30 分钟内不重复发送
  3. 关联分析:比如 CPU 高+丢包率高,可能是编码器出问题了,不要发两条独立的告警

三、大规模并发优化

说到大规模并发,很多人第一反应是加机器。但加机器解决不了所有问题。你想想看,如果每个 PeerConnection 都浪费 10% 的资源,10000 路并发就是 1000 路的浪费。

1. 编码器优化

编码是 WebRTC 最吃 CPU 的环节。我建议:

  • 硬件编码优先:支持 Intel QSV、NVIDIA NVENC 的机器优先用硬件编码,能省 70% 的 CPU
  • 动态码率调整:根据网络状况动态调整编码参数,不要固定码率
  • SVC 分层编码:如果客户端支持,用 SVC 代替 Simulcast,能减少 SFU 的转码压力
// 动态码率调整示例
function onNetworkQuality(quality) {
    let encoder = getVideoEncoder();
    switch(quality) {
        case 'good':
            encoder.setBitrate(2000); // 2Mbps
            encoder.setFrameRate(30);
            break;
        case 'fair':
            encoder.setBitrate(800);  // 800Kbps
            encoder.setFrameRate(24);
            break;
        case 'poor':
            encoder.setBitrate(300);  // 300Kbps
            encoder.setFrameRate(15);
            break;
    }
}

2. 连接复用

每个 PeerConnection 都会占用一个 UDP 端口和一定的内存。如果同一个用户同时加入多个房间,可以考虑复用连接。我在项目中做过一个优化:

  • 同一个用户的所有媒体流走同一个 PeerConnection
  • 通过 SSRC 区分不同的流
  • 这样能减少 40% 的连接数

3. 内存管理

WebRTC 的内存泄漏是个老问题。我建议:

  • 主动释放:用户离开房间后,立即关闭 PeerConnection,不要等 GC
  • 内存池:对于频繁创建销毁的对象(如 RTP 包),用对象池复用
  • 限制缓存:NACK 缓存、Jitter Buffer 的缓存大小要设上限
避坑指南:我曾经遇到过一个问题,用户离开房间后 PeerConnection 没有及时关闭,导致内存泄漏。2000 路并发跑了 3 天,内存从 4G 涨到 32G,最后 OOM 了。排查发现是事件回调里有个引用没释放。

4. 带宽管理

带宽是稀缺资源。一个 1080p 的视频流大约需要 4Mbps,1000 路并发就是 4Gbps。我建议:

  • 限制上行带宽:每个发布者限制最大码率,防止个别用户占用过多带宽
  • 选择性转发:SFU 只转发用户真正需要的流,比如用户只看大屏,就不转发小屏的流
  • 动态降级:当带宽不足时,自动降低分辨率或帧率

四、整体架构图

下面这张图展示了生产环境的完整部署架构。我把它分成三层:接入层、调度层、媒体层。

WebRTC 生产环境部署架构 接入层 Nginx 反向代理 WebSocket 负载均衡 TLS 终止 STUN/TURN 调度层 节点状态管理 房间亲和性调度 媒体层(SFU 集群) SFU 节点 1 SFU 节点 2 SFU 节点 3 ... 监控告警层:Prometheus + Grafana + 告警通知

这张图里,接入层负责处理信令和 NAT 穿透,调度层负责分配 SFU 节点,媒体层负责实际的音视频转发。监控层贯穿整个架构,采集所有节点的指标。

我的建议:刚开始不要追求完美架构。先跑通最小可用版本,然后根据监控数据逐步优化。我见过太多团队花三个月搭架构,结果业务需求变了,架构白做了。

好了,生产环境部署的核心要点就这些。负载均衡解决的是流量分发问题,监控告警解决的是可观测性问题,大规模并发优化解决的是资源效率问题。这三者缺一不可。

公众号:蓝海资料掘金营,微信deep3321