上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-25 08:46 阅读量:977


周三 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

目录结构
全文