ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

一对一视频直播聊天系统架构设计与性能优化实战

一对一视频直播聊天系统架构设计与性能优化实战

1. 一对一视频直播聊天系统的核心架构拆解

一对一视频直播聊天,听起来像是把“直播”和“私密通话”两个场景揉在一起,但真正动手做过的人都知道,这东西的复杂度远不是“开个房间连麦”那么简单。它本质上是一个实时音视频通信系统叠加了直播场景下的状态同步与业务逻辑,同时还要兼顾一对一场景特有的私密性、低延迟和高稳定性要求。我前后参与过三个类似项目的搭建,从早期用开源方案拼凑到后来自己设计信令层,踩过的坑足够写一本小册子。这篇文章就把这套系统的核心逻辑、技术选型、实操步骤和那些文档里不会写的经验,一次性讲透。

先明确一下这个系统到底要做什么。用户A发起通话,用户B接受,双方建立音视频通道,过程中可能还有美颜、滤镜、礼物动画、文字消息、通话录制等附加功能。听起来和普通的视频通话App差不多,但“直播”两个字带来了几个关键差异:第一,主播端和观众端的角色是不对等的,即使是一对一,也可能存在“主播-连麦者”的权限差异;第二,直播场景对首帧耗时极其敏感,用户点下按钮到看到画面,超过1.5秒就会明显感到卡顿;第三,直播往往需要支持旁路推流,也就是把一对一的通话内容同时推送到CDN,供更多观众观看,这就涉及音视频流的混流与转码。

所以,这个项目的核心关键词可以归纳为:WebRTC信令、SFU/MCU架构选型、低延迟传输、状态同步、旁路推流。下面我会从架构设计开始,一步步拆到代码层面,把每个环节的“为什么”和“怎么做”都讲清楚。

1.1 为什么一对一场景反而比多人会议更难做

很多人觉得一对一最简单,两个人连上就行了。但实际项目中,一对一的难点在于没有中间态。多人会议里,某一路流卡了,其他人还能继续,用户体验是渐变的;而一对一通话,只要一方出问题,整个通话就废了。这意味着你对网络抖动、丢包、设备兼容性的处理必须更加鲁棒。

另一个难点是状态同步的实时性。一对一通话中,双方的状态(静音、摄像头开关、网络质量、美颜参数)需要毫秒级同步。比如A点了静音,B那边必须立刻看到麦克风图标变化,否则B会以为A还在说话。这种同步如果走信令服务器绕一圈,延迟可能超过200ms,体验就很差。我们的做法是在音视频通道里开一条DataChannel,专门传这类控制信令,不走WebSocket,实测延迟能压到30ms以内。

还有一个容易被忽略的点:回声消除。一对一场景下,双方很可能都用外放,如果不做AEC(Acoustic Echo Cancellation),对方会听到自己的声音反复回传,根本没法聊。WebRTC自带AEC,但默认参数是针对会议场景调的,一对一通话需要调整echoCancellationType和delayAgnostic参数,这个后面会细说。

1.2 信令层:整个系统的神经中枢

信令层负责交换SDP(Session Description Protocol)和ICE候选地址,是建立连接的第一步。很多教程直接用WebSocket传JSON,这没问题,但生产环境要考虑几个问题:信令的可靠性、顺序性、以及断线重连后的状态恢复。

我们最终选的是WebSocket + 自定义二进制协议。为什么不用Socket.IO?因为Socket.IO的自动重连机制在一对一场景下会带来状态混乱——重连后房间ID可能已经失效,但客户端还在发旧的信令。自己实现重连逻辑反而更可控。信令消息格式用Protobuf序列化,比JSON省30%左右的带宽,解析速度也快一倍。

信令服务器的部署也有讲究。早期我们用了单节点,结果一次机房网络抖动导致所有通话中断。后来改成双节点热备 + Redis Pub/Sub同步状态,任何一个节点挂掉,另一个节点能在3秒内接管。具体做法是:每个通话房间的状态(参与者、SDP、ICE候选)都写入Redis,节点间通过Pub/Sub广播状态变更。客户端重连时,信令服务器从Redis读取最新状态,直接恢复通话,用户几乎无感知。

这里有个坑:Redis的Pub/Sub是“即发即忘”的,如果节点在收到消息前就挂了,状态会丢失。我们的补救措施是定期全量快照 + 增量日志,每5秒把房间状态快照写入Redis的另一个key,重连时先读快照再回放增量日志。这套机制上线后,信令层的可用性从99.5%提升到了99.97%。

2. 音视频引擎选型:WebRTC、SFU还是自研

音视频引擎是整个系统的心脏。市面上主流方案有三类:纯WebRTC P2P、基于SFU的媒体服务器、以及自研RTP栈。一对一场景下,很多人第一反应是用P2P,毕竟只有两个人,直连最省服务器成本。但实际项目中,P2P的NAT穿透成功率只有70%左右,剩下30%需要TURN中继,而TURN中继的带宽成本比SFU还高。更关键的是,P2P模式下你无法做旁路推流,因为媒体流不经过服务器,CDN拿不到数据。

所以我们最终选了SFU架构,具体用的是基于Mediasoup二次开发的媒体服务器。Mediasoup的优势在于它是Node.js库,可以直接嵌入业务逻辑,不像Janus那样需要单独部署和C++扩展。一对一场景下,SFU只做转发,不做混流,延迟可以控制在80ms以内,完全满足直播聊天需求。

2.1 Mediasoup的Worker与Router配置细节

Mediasoup的核心概念是Worker和Router。Worker对应一个CPU核心,Router对应一个房间。一对一场景下,我们给每个通话分配一个独立的Router,通话结束后销毁。这样做的好处是隔离性好,一个通话出问题不会影响其他通话。但缺点是Router创建有开销,实测在4核8G的机器上,创建一个Router大约需要15ms,如果并发通话数超过500,创建开销就会成为瓶颈。

我们的优化方案是Router池化。预先创建一批Router,通话开始时从池里取,结束后不销毁而是重置状态放回池里。重置操作包括清除Transport、Producer、Consumer,耗时约5ms,比重新创建快3倍。池的大小根据并发量动态调整,最小10个,最大200个。监控数据显示,池化后单机支持并发通话数从300提升到了800。

Worker的配置也有讲究。默认情况下,Mediasoup会根据CPU核心数自动创建Worker,但每个Worker的内存占用约为150MB。如果机器内存有限,需要手动限制Worker数量。我们的配置是numWorkers: Math.min(4, os.cpus().length),即最多4个Worker,每个Worker处理200路并发流。超过这个数就加机器,而不是加Worker,因为单Worker的CPU调度瓶颈比内存更早出现。

2.2 编码参数调优:从“能看”到“看得舒服”

WebRTC默认用VP8编码,但VP8在低码率下的表现不如H.264。一对一直播聊天场景,用户网络波动大,我们最终选了H.264 Baseline Profile,因为它的硬件兼容性最好,iOS和Android都能硬解,CPU占用比VP8低40%。

码率控制策略也很关键。默认的CBR(Constant Bitrate)在画面静止时会浪费带宽,我们改成了VBR(Variable Bitrate) + 动态码率调整。具体做法是:初始码率设为800kbps,每2秒根据RTCP Receiver Report里的丢包率和RTT调整一次。丢包率超过5%就降20%码率,低于1%且RTT小于100ms就升10%。这套算法让卡顿率从8%降到了2.3%。

分辨率方面,一对一场景下720p足够,但要注意分辨率自适应。如果检测到对方设备是手机小屏,直接发360p,省带宽也省电。我们的判断逻辑是:通过信令交换设备信息,如果对方是移动端且屏幕小于5英寸,初始分辨率设为480p,后续根据网络情况动态调整。

参数默认值优化值调整理由
编码格式VP8H.264 Baseline硬件兼容性好,CPU占用低
码率模式CBRVBR + 动态调整静止画面省带宽,波动网络更稳
初始码率1000kbps800kbps留出余量应对突发丢包
初始分辨率720p480p(移动端)省带宽,移动端小屏无感知
关键帧间隔3秒2秒加快新用户首帧速度

2.3 音频处理:3A算法的实战调参

音频的3A算法(AEC回声消除、AGC自动增益、ANS噪声抑制)是WebRTC自带的,但默认参数是针对会议场景优化的。一对一通话中,我们调整了以下几个参数:

  • echoCancellationType: 'system':用系统级AEC而不是WebRTC自带的,因为系统级AEC对设备扬声器的建模更准,回声残留少30%。
  • googAutoGainControl: false:关闭自动增益,改用手动增益。因为一对一场景下双方音量差异不大,自动增益反而会导致音量忽大忽小。
  • googNoiseSuppression: true:保留噪声抑制,但把等级从默认的2调到1,避免把呼吸声也消掉。

还有一个细节:音频的抖动缓冲。WebRTC默认的jitter buffer是动态的,但在网络稳定时它会保持一个较大的缓冲,导致延迟增加。我们通过playoutDelayHint参数强制设为0.05秒,实测在稳定网络下音频延迟从120ms降到了60ms,代价是丢包时会有轻微断续,但一对一场景下用户更在意延迟。

3. 从零搭建:环境准备与核心代码实现

这一节直接上实操。假设你已经有一台Linux服务器(Ubuntu 20.04以上),下面是从零搭建一对一视频直播聊天系统的完整步骤。我会把每个命令和配置都写清楚,你照着做就能跑起来。

3.1 服务端环境搭建与Mediasoup编译

首先安装Node.js 16+和必要的编译工具。Mediasoup包含C++原生模块,需要编译,所以Python和make是必须的。

# 安装基础依赖 sudo apt update sudo apt install -y build-essential python3 python3-pip git # 安装Node.js 16 curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - sudo apt install -y nodejs # 验证版本 node -v # 应输出 v16.x npm -v # 应输出 8.x

接下来创建项目并安装Mediasoup。注意Mediasoup的版本要和Node.js版本匹配,Node 16对应Mediasoup 3.9.x。

mkdir live-chat && cd live-chat npm init -y npm install mediasoup@3.9.0 express socket.io

安装Mediasoup时如果报错“Python not found”,检查python3是否在PATH里。如果报错“gcc version too old”,升级gcc到9以上。编译过程大约需要3-5分钟,取决于机器性能。

编译完成后,创建一个简单的Worker启动脚本验证环境:

// server.js const mediasoup = require('mediasoup'); (async () => { const worker = await mediasoup.createWorker({ logLevel: 'warn', rtcMinPort: 40000, rtcMaxPort: 40100, }); console.log('Worker created, PID:', worker.pid); worker.on('died', () => { console.error('Worker died, exiting'); process.exit(1); }); })();

运行node server.js,如果输出“Worker created”就说明环境OK。这里rtcMinPort和rtcMaxPort是RTP端口范围,一对一场景下100个端口足够支持50路并发通话(每路通话占用2个端口)。

3.2 信令服务器的WebSocket实现

信令服务器用Socket.IO实现,虽然前面说生产环境建议自研,但原型阶段Socket.IO最快。核心逻辑是:客户端加入房间、交换SDP、交换ICE候选、离开房间。

// signaling.js const io = require('socket.io')(server, { cors: { origin: '*' }, pingTimeout: 5000, pingInterval: 2000, }); const rooms = new Map(); // roomId -> Set<socketId> io.on('connection', (socket) => { socket.on('join', ({ roomId, userId }) => { if (!rooms.has(roomId)) rooms.set(roomId, new Set()); const room = rooms.get(roomId); if (room.size >= 2) { socket.emit('room-full'); return; } room.add(socket.id); socket.join(roomId); socket.roomId = roomId; socket.userId = userId; // 通知房间内其他人 socket.to(roomId).emit('peer-joined', { userId }); // 如果房间已有两人,通知发起方开始协商 if (room.size === 2) { io.to(roomId).emit('ready'); } }); socket.on('offer', ({ roomId, sdp }) => { socket.to(roomId).emit('offer', { sdp, from: socket.userId }); }); socket.on('answer', ({ roomId, sdp }) => { socket.to(roomId).emit('answer', { sdp, from: socket.userId }); }); socket.on('ice-candidate', ({ roomId, candidate }) => { socket.to(roomId).emit('ice-candidate', { candidate, from: socket.userId }); }); socket.on('disconnect', () => { const roomId = socket.roomId; if (roomId && rooms.has(roomId)) { const room = rooms.get(roomId); room.delete(socket.id); socket.to(roomId).emit('peer-left', { userId: socket.userId }); if (room.size === 0) rooms.delete(roomId); } }); });

这里有个关键点:pingTimeout和pingInterval要设小一点。默认的20秒和25秒太长,一对一通话中如果一方断网,另一方要等20秒才知道,体验很差。改成5秒和2秒后,断线检测时间缩短到7秒以内。

3.3 客户端音视频采集与PeerConnection建立

客户端用原生WebRTC API,不依赖任何框架。核心流程是:获取本地流、创建PeerConnection、添加Track、交换SDP、交换ICE。

// client.js const socket = io('https://your-server.com'); let pc = null; let localStream = null; async function startCall(roomId, userId) { // 1. 获取本地音视频 localStream = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 }, }, audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: false, }, }); document.getElementById('localVideo').srcObject = localStream; // 2. 创建PeerConnection pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, // 生产环境需要加TURN服务器 ], iceTransportPolicy: 'all', bundlePolicy: 'max-bundle', }); // 3. 添加本地Track localStream.getTracks().forEach((track) => { pc.addTrack(track, localStream); }); // 4. 处理远端流 pc.ontrack = (event) => { document.getElementById('remoteVideo').srcObject = event.streams[0]; }; // 5. 处理ICE候选 pc.onicecandidate = (event) => { if (event.candidate) { socket.emit('ice-candidate', { roomId, candidate: event.candidate }); } }; // 6. 加入房间 socket.emit('join', { roomId, userId }); // 7. 监听信令 socket.on('ready', async () => { const offer = await pc.createOffer(); await pc.setLocalDescription(offer); socket.emit('offer', { roomId, sdp: offer }); }); socket.on('offer', async ({ sdp }) => { await pc.setRemoteDescription(new RTCSessionDescription(sdp)); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); socket.emit('answer', { roomId, sdp: answer }); }); socket.on('answer', async ({ sdp }) => { await pc.setRemoteDescription(new RTCSessionDescription(sdp)); }); socket.on('ice-candidate', async ({ candidate }) => { await pc.addIceCandidate(new RTCIceCandidate(candidate)); }); }

这段代码能跑通基本的一对一通话,但生产环境还需要处理重协商(比如中途开启屏幕共享)、码率控制、网络切换(WiFi切4G)等情况。重协商的逻辑是:当本地Track发生变化时,调用pc.createOffer({ iceRestart: true })重新协商,对方收到新Offer后更新RemoteDescription。

3.4 旁路推流的配置与踩坑

旁路推流是把一对一通话的内容推送到CDN,让更多观众观看。Mediasoup本身不支持推流,需要配合FFmpeg或GStreamer。我们的做法是:在Mediasoup里创建一个PlainTransport,把音视频流导出到本地端口,然后用FFmpeg拉流并推送到CDN。

// 创建PlainTransport用于推流 const transport = await router.createPlainTransport({ listenIp: '127.0.0.1', rtcpMux: true, comedia: false, }); // 创建Consumer,把Producer的流导出到PlainTransport const consumer = await transport.consume({ producerId: videoProducer.id, rtpCapabilities: router.rtpCapabilities, paused: false, }); // 获取Consumer的RTP参数 const { rtpPort, rtpParameters } = consumer; console.log('RTP port:', rtpPort);

然后启动FFmpeg:

ffmpeg -re -i rtp://127.0.0.1:5000 \ -c:v libx264 -preset ultrafast -tune zerolatency \ -c:a aac -b:a 128k \ -f flv rtmp://your-cdn.com/live/streamkey

这里有几个坑:第一,rtpPort是动态分配的,需要从Consumer里读取后传给FFmpeg,不能写死。第二,FFmpeg的-preset ultrafast和-tune zerolatency必须加,否则延迟会超过3秒。第三,如果CDN要求RTMP over TLS,FFmpeg需要加-rtmp_ssl 1参数。第四,推流过程中如果Producer暂停(比如用户关摄像头),FFmpeg会收到空白帧,需要在FFmpeg里加-vf "setpts=PTS-STARTPTS"避免时间戳错乱。

4. 上线后才懂的稳定性与体验优化

代码跑通只是第一步,真正上线后遇到的问题才是考验。这一节分享几个我们踩过的坑和对应的解决方案,都是文档里不会写的。

4.1 首帧耗时从2秒压到800毫秒的完整过程

首帧耗时是直播聊天最核心的体验指标。用户点下“开始通话”到看到对方画面,如果超过1.5秒,就会觉得“卡了”。我们最初的首帧耗时是2.1秒,经过三轮优化降到了800ms。

第一轮优化:预创建PeerConnection。用户进入通话页面时,不等点击按钮,就提前创建好PeerConnection并获取本地流。点击按钮后只需要交换SDP,省掉了设备采集的300-500ms。这一轮降到1.6秒。

第二轮优化:ICE候选预收集。默认情况下,ICE候选是在创建Offer后才开始收集的,耗时约200ms。我们改成在预创建PeerConnection时就调用pc.createOffer()并setLocalDescription(),让ICE收集提前开始。等真正通话时,候选已经收集好了,直接发送。这一轮降到1.1秒。

第三轮优化:SDP精简。WebRTC的SDP默认包含大量编解码器信息,约4KB。我们通过setCodecPreferences只保留H.264和Opus,SDP缩小到1.2KB,传输和解析时间减少60%。同时把iceTransportPolicy设为relay,跳过主机候选的收集和测试,直接走TURN中继。虽然增加了20ms的中继延迟,但省掉了候选测试的300ms。这一轮降到800ms。

优化阶段措施首帧耗时降幅
初始无2100ms-
第一轮预创建PeerConnection1600ms24%
第二轮ICE候选预收集1100ms31%
第三轮SDP精简 + relay模式800ms27%

4.2 弱网环境下的卡顿排查与修复

弱网是直播聊天的头号杀手。我们遇到过用户反馈“画面卡住不动,声音正常”,排查后发现是视频的NACK重传机制失效。WebRTC默认开启NACK,但Mediasoup的默认配置里maxRetransmits是0,意味着不重传。改成maxRetransmits: 3后,弱网下的视频卡顿率从15%降到了4%。

另一个问题是带宽估计不准。WebRTC的带宽估计基于RTCP反馈,但在高丢包环境下会低估带宽,导致码率降得太低,画面模糊。我们的解决方案是手动设置最小码率,通过RTCRtpSender.setParameters()把minBitrate设为300kbps,保证画面至少能看清人脸。

还有一个隐蔽的坑:TURN服务器的UDP端口耗尽。TURN中继需要为每个通话分配一个UDP端口,默认范围是49152-65535,约16000个端口。如果并发通话超过8000,端口就不够用了。我们的做法是复用TURN端口,通过TURN REST API让多个通话共享同一个端口,用不同的username区分。这样单台TURN服务器可以支持5万路并发。

4.3 回声消除失效的三种场景与修复

回声消除失效是音频问题里最让人头疼的。我们遇到过三种场景:

第一种是蓝牙耳机切换。用户从手机听筒切到蓝牙耳机时,系统音频路由变了,但WebRTC的AEC还在用旧的设备参数,导致回声。修复方法是监听devicechange事件,在设备切换时重新初始化音频轨道。

第二种是外放音量过大。当扬声器音量超过80%时,AEC的残留回声会明显增加。我们的做法是在UI上提示用户“建议音量不超过70%”,同时在音频处理链里加一个DynamicsCompressorNode,把输出音量限制在-6dB以内。

第三种是双讲场景。双方同时说话时,AEC会把一方的人声也当成回声消掉,导致声音断续。这是AEC算法的固有限制,我们的缓解方案是动态调整AEC的收敛速度,通过echoCancellationType: 'system'让系统级AEC处理双讲,比WebRTC自带的AEC好30%左右。

4.4 移动端后台切换与断线重连

移动端有个特殊问题:App切到后台后,WebRTC的音视频采集会被系统暂停,但PeerConnection还保持着。如果用户切回前台,画面会黑屏几秒。我们的解决方案是监听visibilitychange事件,在页面隐藏时主动暂停视频轨道(track.enabled = false),切回时重新启用。这样虽然会短暂黑屏,但比PeerConnection断开再重连快得多。

断线重连的逻辑也要特别处理。移动网络切换(WiFi切4G)会导致ICE连接断开,但PeerConnection不会自动恢复。我们的做法是监听iceconnectionstatechange,当状态变为disconnected时,等待3秒,如果还没恢复就调用pc.restartIce()触发ICE重启。ICE重启会重新收集候选并协商,整个过程约1.5秒,用户感知就是画面卡了一下。

5. 业务功能扩展与性能压测数据

基础通话跑通后,业务方一定会提各种需求:美颜、滤镜、礼物动画、通话录制、文字消息。这些功能怎么加,加在哪里,对性能有什么影响,这一节用实测数据说话。

5.1 美颜滤镜的GPU加速方案

美颜滤镜如果用CPU处理,720p@30fps的视频需要至少4核CPU才能实时处理,手机端根本扛不住。我们的方案是用WebGL着色器在GPU上做美颜,具体流程是:把视频帧上传到纹理,用着色器做磨皮、美白、瘦脸,再输出到Canvas,最后通过canvas.captureStream()生成新的视频轨道替换原始轨道。

实测下来,iPhone 12上GPU美颜的耗电增加约8%,CPU占用增加5%,完全可以接受。Android中端机(骁龙7系)上耗电增加15%,CPU占用增加12%,需要把美颜等级默认调低一档。

这里有个坑:canvas.captureStream()生成的轨道在部分Android机型上帧率不稳定,会掉到15fps。我们的修复方案是手动控制帧率,用requestAnimationFrame驱动渲染,并在每帧渲染完成后调用track.requestFrame()强制输出。这样帧率稳定在25-30fps。

5.2 礼物动画与音视频的同步策略

礼物动画是直播聊天的核心营收功能,但动画和音视频的同步很容易出问题。如果动画走WebSocket信令,延迟可能超过500ms,用户看到动画时对方已经说完了。我们的做法是把动画指令走DataChannel,和音视频流共用同一条传输通道,延迟压到50ms以内。

DataChannel的配置也有讲究。默认的ordered: true会保证消息顺序,但会增加延迟。礼物动画不需要严格顺序,我们设成ordered: false, maxRetransmits: 0,走UDP语义,延迟最低。但要注意,maxRetransmits: 0意味着消息可能丢失,所以动画指令要设计成幂等的,丢了就丢了,不影响通话。

5.3 通话录制:从服务端录制到云端录制

通话录制有两种方案:服务端录制和云端录制。服务端录制是在Mediasoup里把音视频流导出到文件,优点是免费,缺点是占用服务器CPU和磁盘。云端录制是推流到云服务商的录制服务,优点是省事,缺点是按分钟计费。

我们最终选了服务端录制,因为一对一场景下录制文件不大(1小时约500MB),服务器磁盘完全扛得住。具体实现是:在Mediasoup里创建一个PlainTransport,把音视频流导出到本地端口,然后用FFmpeg录制为MP4文件。FFmpeg的命令和旁路推流类似,只是输出从RTMP改成文件:

ffmpeg -re -i rtp://127.0.0.1:5000 \ -c:v copy -c:a copy \ -f mp4 /recordings/room_123.mp4

这里用-c:v copy直接复制视频流,不重新编码,CPU占用几乎为零。但要注意,如果音视频流的时间戳不连续,MP4文件会损坏。我们的修复方案是加-fflags +genpts参数,让FFmpeg自动生成时间戳。

5.4 压测数据:单机并发与资源消耗

最后分享一组压测数据,供你做容量规划时参考。测试环境是4核8G的云服务器,Mediasoup配置为2个Worker,每个Worker 2个核心。

并发通话数CPU占用内存占用带宽(上行)首帧耗时(P95)卡顿率
10025%1.2GB80Mbps850ms1.2%
30055%2.8GB240Mbps920ms2.1%
50082%4.5GB400Mbps1100ms3.8%
80095%6.8GB640Mbps1500ms6.5%

从数据看,单机500路并发是性价比最高的点,CPU还有余量,卡顿率也可接受。超过500路后,CPU成为瓶颈,首帧耗时和卡顿率都明显上升。我们的扩容策略是按500路一档线性扩容,同时预留20%的余量应对突发流量。

内存方面,每个通话约占9MB,主要是PeerConnection和Transport的开销。如果内存紧张,可以通过worker.close()定期重启Worker释放内存,但重启会导致该Worker上的通话中断,所以要在低峰期做。

带宽是容易被忽略的成本。500路并发时上行带宽400Mbps,如果云服务器按流量计费,一小时的成本相当可观。我们的优化方案是开启BWE(Bandwidth Estimation)和Pacing,让发送速率更平滑,减少突发流量。实测开启后带宽峰值降低15%,平均带宽降低8%。

这套系统从零到上线用了约6周,其中前2周在搭环境和调通基础通话,中间2周在优化首帧和弱网体验,最后2周在加业务功能和压测。如果让我重新做一遍,我会把信令层的状态同步和弱网优化放在最前面,因为这两块一旦设计不好,后期改起来伤筋动骨。另外,TURN服务器的部署千万别省,P2P穿透率在实际网络环境下只有70%左右,剩下的30%全靠TURN兜底,没有TURN的系统根本没法上线。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进