香港服务器 Ubuntu 20.04 部署 WebRTC + Coturn 中继优化跨境课堂卡顿:完整安装配置、网络调优、压测数据与故障复

周三 22:40,深圳一所学校的老师在群里急了——“今晚公开课,视频一卡一卡的,学生说声音像机器人。”我看了下链路:老师在深圳,学生分散在成都、青岛、宁波,WebRTC 直连走公网,UDP 在部分校园网被限,TURN 没配好。
我背上工具包,直奔葵涌的香港机房。冷通道的风像刀一样,指示灯一排排亮着。接下来四小时,我在一台 Ubuntu 20.04 的香港节点上,拉起了 Coturn 中继、调了内核网络栈、给 WebRTC 端加了正确的 ICE 策略;到了午夜 01:35,老师再连,“不卡了,几乎零延迟”。这篇就是那次落地的完整过程与细节复盘。
一、场景与目标
业务场景:跨境互动课堂(老师端在内地,学生端分布多城/海外),浏览器 WebRTC(H.264 + Opus),需要穿透校园网/企业网的复杂 NAT/防火墙。
主要问题:
UDP 在部分网段被限或丢包大,P2P 直连失败,ICE 长时间探测导致入会慢、黑屏。
TURN 配置不当(端口/证书/鉴权/端口段),导致中继失败或回退 TCP 但被 443 端口冲突卡住。
服务器侧队列/拥塞控制没调,跨境链路抖动大(jitter 20–60ms),音视频同传时突发丢包。
目标:
在香港机房部署一台 Ubuntu 20.04 节点(低延时跨境),提供 STUN+TURN(TCP/TLS/UDP)。
WebRTC 端强制正确的 ICE 优先级 与 TCP/TLS 回落策略;必要时可只走中继(relay-only)。
系统/内核、队列、端口段、证书与监控完善,可灰度、可回滚。
二、硬件与网络选型(我用的这台)
| 项目 | 参数 |
|---|---|
| 机房 | 香港(BGP,多线优化,内地方向 CN2/GIA/CMI 优先) |
| 服务器 | 1×AMD EPYC 7302P(或同档 8C/16T),32GB RAM,2×NVMe 960GB(RAID1) |
| 网卡 | 2×10GbE(主用 1×10G,备 1×10G),物理上联 LACP(可选) |
| 公网 | 2 个公网 IPv4(建议:一个专用给 TURN 443/TCP/TLS,一个给 Web/管理) |
| 带宽 | 上下行对等 1–10Gbps(本次压测峰值 ~800 Mbps 足够) |
| 操作系统 | Ubuntu 20.04.6 LTS(5.4 系列内核) |
| 时钟 | chrony 与香港授时源对齐,NTP 偏差 < 5ms |
经验:双 IP 能省掉很多 443 复用的麻烦;如果只有一个 IP,也可以用 Nginx stream 做 443 分流,但 TURN/TLS 的 SNI/ALPN 不如纯独占稳妥。
三、系统准备与网络内核调优
1)基础与时间同步
sudo apt update && sudo apt -y upgrade
sudo apt -y install chrony jq curl vim htop net-tools mtr iperf3 tcpdump
sudo systemctl enable --now chrony
chronyc tracking # 确认偏差
2)内核参数(BBR + 队列 + 缓冲)
编辑 /etc/sysctl.d/99-realtime-webrtc.conf:
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
# 放大 Socket 缓冲(注意机器内存)
net.core.rmem_max=67108864
net.core.wmem_max=67108864
net.core.rmem_default=262144
net.core.wmem_default=262144
net.ipv4.udp_mem= 10240 87380 67108864
net.ipv4.udp_rmem_min=16384
net.ipv4.udp_wmem_min=16384
# 减少重路由开销
net.ipv4.ip_local_port_range=20000 65000
# conntrack 放宽(适量)
net.netfilter.nf_conntrack_max=1048576
# 关闭反向路径严格检查(双网卡多路径时必要)
net.ipv4.conf.all.rp_filter=0
net.ipv4.conf.default.rp_filter=0
生效:
sudo sysctl --system
3)防火墙(ufw/nftables 二选一)
示例(ufw):
sudo apt -y install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
# STUN/TURN 常用端口
sudo ufw allow 3478/udp # TURN/STUN UDP
sudo ufw allow 3478/tcp # TURN TCP
sudo ufw allow 5349/tcp # TURN over TLS
sudo ufw allow 443/tcp # 若 TURN 走 443/TCP/TLS
sudo ufw allow 49152:65535/udp # TURN relay 端口段
# 管理与 Web
sudo ufw allow 22/tcp
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status
四、安装与配置 Coturn(核心)
1)安装
sudo apt -y install coturn
# Ubuntu 默认不会自动启用,需要在 /etc/default/coturn 打开开关
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
2)准备证书(Let’s Encrypt 示例)
sudo apt -y install nginx certbot python3-certbot-nginx
sudo certbot certonly --nginx -d turn.example.com # 替换你的域名
# 证书一般在 /etc/letsencrypt/live/turn.example.com/
3)/etc/turnserver.conf(我线上使用的基线)
# 监听
listening-port=3478
tls-listening-port=5349
# 专用 IP(建议 TURN 使用独立公网 IP)
listening-ip=203.0.113.10
relay-ip=203.0.113.10
# 中继端口段(与防火墙一致)
min-port=49152
max-port=65535
# 域与指纹
realm=classroom.example.com
fingerprint
# 长期鉴权 - 使用 REST Secret(推荐)
use-auth-secret
static-auth-secret=CHANGE_ME_TO_LONG_RANDOM_BASE64 # 与应用端一致
# 用户名结构推荐: timestamp:userid
# example: 1737500000:teacher-001
# TLS
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem
no-tlsv1
no-tlsv1_1
cipher-list="HIGH:!aNULL:!MD5"
# 安全
no-loopback-peers
no-multicast-peers
# 如需限制已知恶意网段:
# denied-peer-ip=100.64.0.0-100.127.255.255
# 日志
log-file=/var/log/turnserver/turn.log
simple-log
# 性能
proc-user=turnserver
proc-group=turnserver
# 多进程与线程(默认即可;高并发可按 CPU 调整)
注意:如果你需要 443/TCP/TLS 给 TURN(更能穿企业防火墙),把 tls-listening-port=5349 改成 443,同时确保 443 不被 Nginx 占用(或者 TURN 用第二个公网 IP 绑定 443)。
4)启用并验证
sudo mkdir -p /var/log/turnserver && sudo chown turnserver:turnserver /var/log/turnserver
sudo systemctl enable --now coturn
sudo systemctl status coturn
sudo ss -lptn | grep -E '3478|5349|443'
连通性检查:
# TURN/TLS
openssl s_client -connect turn.example.com:5349 -servername turn.example.com </dev/null
# TURN TCP
nc -vz turn.example.com 3478
# TURN UDP(只能用工具测,或从客户端 ICE 观察)
五、应用端(WebRTC)配置要点
1)TURN REST 临时凭证(Node.js 样例)
原则:不要在前端写死 TURN 用户/密码,用后端签发短期凭证(2–10 分钟有效)。
// server/turn-token.js
import crypto from 'crypto';
import express from 'express';
const app = express();
const TURN_SECRET = process.env.TURN_SECRET || 'CHANGE_ME_TO_LONG_RANDOM_BASE64';
const TURN_HOST = 'turn.example.com';
app.get('/api/turn-credential', (req, res) => {
const userId = req.query.uid || 'guest';
const ttl = 600; // 10 min
const username = `${Math.floor(Date.now()/1000) + ttl}:${userId}`;
const hmac = crypto.createHmac('sha1', TURN_SECRET);
hmac.update(username);
const credential = hmac.digest('base64');
res.json({
username,
credential,
iceServers: [
{ urls: [`stun:${TURN_HOST}:3478`] },
// UDP 优先,其次 TCP/TLS,最后 443/TCP 兜底
{ urls: [`turn:${TURN_HOST}:3478?transport=udp`], username, credential },
{ urls: [`turn:${TURN_HOST}:3478?transport=tcp`], username, credential },
{ urls: [`turns:${TURN_HOST}:5349?transport=tcp`], username, credential },
{ urls: [`turns:${TURN_HOST}:443?transport=tcp`], username, credential }
]
});
});
app.listen(3000, () => console.log('TURN token service on :3000'));
2)前端 WebRTC(关键 ICE 策略)
// 获取后端签发的临时 TURN 凭证
const resp = await fetch('/api/turn-credential?uid=' + encodeURIComponent(userId));
const { iceServers } = await resp.json();
const pc = new RTCPeerConnection({
iceServers,
iceCandidatePoolSize: 4, // 加快收敛
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require'
});
// 对于“必须成功优先”的课堂场景:必要时可强制中继
// const pc = new RTCPeerConnection({ iceServers, iceTransportPolicy: 'relay' });
// Trickle ICE
pc.onicecandidate = e => {
if (e.candidate) signal('ice', e.candidate);
};
// 视频编码降级策略:课堂以“稳”为主
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
video: {
width: { ideal: 1280, max: 1280 },
height: { ideal: 720, max: 720 },
frameRate: { ideal: 15, max: 24 }
}
});
stream.getVideoTracks()[0].applyConstraints({
width: 1280,
height: 720,
frameRate: 15
});
pc.addTrack(stream.getAudioTracks()[0], stream);
pc.addTrack(stream.getVideoTracks()[0], stream);
// 如果用 SFU(可选),在 sender 上设置码率上限/分层
const senders = pc.getSenders().filter(s => s.track && s.track.kind === 'video');
for (const s of senders) {
const params = s.getParameters();
params.encodings = [{ maxBitrate: 800_000, scalabilityMode: 'L3T3_KEY' }];
await s.setParameters(params);
}
课堂业务经验:语音优先。720p/15fps 足够清楚黑板,码率 700–900 kbps。若链路抖动大,优先保音频、降视频帧率或分辨率。
六、443 端口复用的坑与规避
最佳:TURN 独占第二个公网 IP 的 443/TCP/TLS,Web 走另一个 IP 的 443。
只有一个 IP:可以尝试 Nginx stream 分流,但 TURN/TLS 的握手不携带可分流的明确标识时(某些客户端实现),会出现误判。退一步:TURN 用 5349/TLS + 3478/TCP,再在前端多配一个 turns:...:443?transport=tcp 做兜底,仅在确认可用时启用。
/etc/nginx/streams-enabled/turn.conf(仅供单 IP 折中参考):
stream {
map $ssl_preread_server_name $backend {
turn.example.com turn_backend;
default web_backend;
}
upstream turn_backend { server 127.0.0.1:5349; } # coturn TLS
upstream web_backend { server 127.0.0.1:8443; } # 你的 Web
server {
listen 443 reuseport;
ssl_preread on;
proxy_pass $backend;
}
}
提示:并非所有 TURN/TLS 客户端都提供 SNI,分流失效时请改为双 IP或让 TURN 直接监听 443,Web 改非标端口/走 H3。
七、可选:上 SFU 减少上行压力(mediasoup/Janus)
当老师端上行只有 5–10 Mbps,又要 1:N 推流时,SFU 很必要。这里给一个 mediasoup 的最小化 docker-compose 片段(只做思路引导):
version: "3.8"
services:
sfu:
image: ghcr.io/versatica/mediasoup-demo:v3
network_mode: host
environment:
- MEDIASOUP_LISTEN_IP=203.0.113.10
- MEDIASOUP_ANNOUNCED_IP=203.0.113.10
- MEDIASOUP_MIN_PORT=40000
- MEDIASOUP_MAX_PORT=49999
volumes:
- ./config:/app/config
restart: unless-stopped
将 SFU 与 Coturn 同机时要错开端口段(如 40000–49999 给 SFU,49152–65535 给 TURN)。
上线前做 端到端延迟 与 多房间并发的压测(建议 wrtc 机器人或自研脚本)。
八、观测与压测(我当晚的验证步骤)
链路基线:
mtr -rwzc 100 student.ip,观测抖动/丢包;iperf3 -u -b 50M -l 1200 -t 30 测 UDP 抗抖。
ICE 状态:
Chrome chrome://webrtc-internals 看 candidate type,确保在受限网段能回落到 relay,连通建立 < 2s。
Coturn 日志:
journalctl -u coturn -f 观察 ALLOCATE, BINDING,确认中继分配成功且端口段足够。
TLS 检查:
openssl s_client 检证书链(务必用 fullchain.pem),避免“中间证书缺失”导致的握手失败。
九、优化项清单(效果显著的那几条)
ICE 列表顺序:先 turn:3478?transport=udp,再 tcp,再 turns:5349/443。
REST 临时凭证:10 分钟有效 + 用户 ID 绑定,降低泄露风险。
BBR + fq:跨境拥塞明显缓解,码率更稳,音频抖动从 40ms 降到 ~12ms。
端口段放宽:49152–65535/udp 至少 16k 端口;并发教室多时更稳。
双 IP:彻底解决 443 冲突与分流不准的问题。
SFU(可选):老师 1 路上行,服务端分发,弱网端显著收益。
编解码策略:课堂以清晰+稳定为第一目标,720p/15fps/800kbps + Opus 48k 是大多数场景的甜点位。
十、数据对比(复盘当晚)
| 指标 | 优化前(直连+无正确 TURN) | 优化后(香港 TURN + 调优) |
|---|---|---|
| 首帧时间(新入会) | 4.8 s | 1.9 s |
| 端到端延迟(中位数) | 320 ms | 165 ms |
| 抖动(P95) | 58 ms | 14 ms |
| 丢包(P95) | 4.5% | 0.7% |
| 码率稳定性(±20% 以内时间占比) | 62% | 91% |
| “机器人音”投诉次数(每课时) | 3–5 次 | 0–1 次 |
采样:深圳/成都/青岛 9 位学生 + 老师端 1 位,45 分钟课程与 20 分钟彩排,Chrome/Edge/移动端混合。
十一、常见坑位与解决
443 被 Nginx 占用
解法 A:TURN 走第二公网 IP 的 443。
解法 B:Web 改 8443,443 专给 TURN。
解法 C(折中):Nginx stream 分流,但要接受偶发误判风险。
证书链不全导致 TURN/TLS 握手失败
确保使用 fullchain.pem;openssl s_client -showcerts 核对中间证书。
校园网仅放行 80/443/TCP,UDP 全禁
前端 务必配置 turns:...:443?transport=tcp,必要时 iceTransportPolicy: 'relay' 强制中继。
端口段太小
TURN relay 端口段并发不够会“随机”掉线,至少开到 49152–65535。
conntrack 不足
增大 nf_conntrack_max,并监控 /proc/sys/net/netfilter/nf_conntrack_count。
STUN 当 TURN 用
STUN 只做公网探测,不转发媒体。课堂要可用,必须 TURN 中继兜底。
双网卡/多出口路由异常
rp_filter=0 并确保 listening-ip/relay-ip 明确绑定到出口。
十二、上线节奏与回滚
灰度:先让一个班级/一个教师群体使用 relay 优先,监控 24 小时。
监控:拉取 coturn 日志,统计 ALLOCATE 成功率/时延、peer connection 建立时间、candidate type 分布。
回滚:保留旧配置文件 turnserver.conf.bak,systemd 切换后 1 分钟内即可回滚。前端保留直连策略作为最后兜底。
十三、成本估算(便于和老板沟通)
| 成本项 | 说明 | 量级 |
|---|---|---|
| 香港服务器 | 8C/32G/1–10Gbps/双 IP | 常见市场价中等水平 |
| 域名与证书 | 域名费用 + LE 免费证书 | 低 |
| 运维 | 初装 0.5–1 人天 + 例行巡检 | 低–中 |
| 带宽 | 与并发/画质挂钩(课表密度大时显著) | 中–高(可弹性) |
经验:对课堂类业务,稳定>极致清晰。带宽花在“兜底”和“均衡”上,比追求 1080p 更值。
十四、完整部署速记(我当晚的最小可用路径)
装系统 + 更新 + chrony
sysctl:BBR + fq + UDP 缓冲 + conntrack
防火墙:开 3478/{udp,tcp}、5349/tcp、443/tcp(视方案)、49152–65535/udp
证书:turn.example.com LE 证书
Coturn:use-auth-secret,大端口段,双 IP 优选
应用端:REST 临时凭证,ICE 顺序合理,必要时 relay-only
验证:openssl s_client / webrtc-internals / journalctl -f
灰度观察:采样三城市,45 分钟课堂
凌晨 01:35 的“谢谢老师再见”
那晚我把电脑合上时,教室里最后一句“谢谢老师再见”从监控端清清楚楚传出来。没有电流声,没有机器人音。冷通道的风还是冷,但心里松了口气。
这些年做运维,我越来越相信:稳定不是玄学,是一堆细节拼起来的结果。香港这一跳不神秘,Coturn 也不复杂,难的是把端口、证书、策略、拥塞、链路这些小齿轮一个个拧到位。愿这篇实操能帮你在关键时刻稳住课堂,让每一句“再见”都清晰可闻。
附录:关键文件与命令清单
/etc/turnserver.conf(核心配置,见上文)
/etc/sysctl.d/99-realtime-webrtc.conf(网络栈调优,见上文)
签发 TURN 凭证服务(Node.js 片段,见上文)
验证命令:
chronyc tracking
mtr -rwzc 100 X.X.X.X
iperf3 -u -c server -b 50M -l 1200 -t 30
openssl s_client -connect turn.example.com:5349 -servername turn.example.com </dev/null
journalctl -u coturn -f