从 P2P 崩到稳:香港Ubuntu服务器部署Janus SFU的实战指南(含 TURN 与 simulcast)
技术教程 2025-09-21 10:32 242


午夜 1:40,我戴着工卡刷进香港葵涌的机房。白板上画着一张“老师在深圳、学生散落华南/东南亚”的拓扑,旁边贴着今天的事故:5 节互动课掉线 17 次,P2P 房间在 10+ 人时崩盘。

我当场拍板:把房间切到 Janus SFU(VideoRoom 插件),服务器放在香港(对内地/东南亚时延都能接受),客户端一律强制带上自建 TURN 兜底。SFU 的路由转发 + 浏览器侧 simulcast,我赌它能稳住。事实证明,这一把赌对了(后文有完整配置与对比数据)。
(VideoRoom 是 Janus 的 SFU 插件,负责“多人房间音视频转发”,不是混流 MCU。官方文档说得很清楚。)

1)硬件与带宽怎么挑:别被“核多=能打”骗了

场景目标:单房 12–20 人互动、双师/助教轮换、常态 720p、大图+小窗切换顺滑,在网络不太好的家庭宽带也能稳住 15–20fps 的流畅感。

我的机器清单(香港本地机房,单台)

项目 规格 选择理由
CPU 16 vCPU(AMD EPYC 或 Intel Xeon 同级),单核 ≥ 3.0GHz SFU 不转码但要做大量包转发、NACK/FEC/PLI 处理,主频比“核多”更值钱
内存 32–64 GB 足够的缓冲与多房并发
磁盘 1TB NVMe 主要放日志/录制/转储,IO 快排障友好
网卡 1×1/10 Gbps,支持多队列+RSS 让软中断压力分散到多核
线路 香港本地多线(CMI/CTG/CU 直连优先) 低 RTT对互动课堂比带宽更关键
  • 发布端(主讲 1.2–1.6 Mbps、学员 0.2–0.4 Mbps);
  • 订阅端(每人平均拉 2–4 路,合计 1–3 Mbps)。

SFU 汇总带宽:下行 ≈ Σ订阅 ≈ 15–30 Mbps/房,上行 ≈ Σ发布 ≈ 5–10 Mbps/房。

单机 1Gbps 能跑 20–30 个房是常态(保守算,留足突发/NACK)。

2)系统与网络底座:Ubuntu + UDP 友好参数

系统:Ubuntu 22.04/24.04 LTS 都测过。先把内核与时钟打牢,否则后面全白忙。

# 基础
apt update && apt -y upgrade
apt -y install build-essential curl git htop iftop iotop ca-certificates chrony jq

# UDP 友好内核参数(/etc/sysctl.d/99-janus.conf)
cat >/etc/sysctl.d/99-janus.conf <<'EOF'
fs.file-max = 1048576
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.netdev_max_backlog = 250000
net.ipv4.udp_rmem_min = 4096
net.ipv4.udp_wmem_min = 4096
net.ipv4.ip_local_port_range = 10000 65535
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_congestion_control = bbr
EOF
sysctl --system

# 打开文件句柄(/etc/security/limits.conf)
echo -e "* soft nofile 1048576\n* hard nofile 1048576" >> /etc/security/limits.conf

时钟:chrony 同步到就近香港源,Jitter 明显更稳。

防火墙:只放行 Janus API(8088/8089/8188/8989,后面会反代)、以及 Janus 媒体 UDP 端口段(我们用 20000–24000)。Janus 的 UDP 端口段可在配置里指定(rtp_port_range 属于 WebRTC 连接所用端口段)。

3)编译 Janus(别偷懒,关键库要新)

官方 README 已经明确:libnice 建议用较新的版本(最好自行编译 master),libsrtp 也建议 2.x;否则各种 ICE/DTLS 小坑会折磨你。

# 依赖
apt -y install libmicrohttpd-dev libjansson-dev libssl-dev libsofia-sip-ua-dev \
  libglib2.0-dev libopus-dev libogg-dev libcurl4-openssl-dev liblua5.3-dev \
  libconfig-dev pkg-config libtool automake cmake autoconf libwebsockets-dev

# libnice(强烈建议从源构建)
git clone https://gitlab.freedesktop.org/libnice/libnice.git
cd libnice && meson --prefix=/usr build && ninja -C build && ninja -C build install
ldconfig && cd ..

# libsrtp2(若系统版本不新)
wget https://github.com/cisco/libsrtp/archive/refs/tags/v2.2.0.tar.gz
tar xf v2.2.0.tar.gz && cd libsrtp-2.2.0
./configure --prefix=/usr --enable-openssl && make -j && make install
ldconfig && cd ..

# 可选:usrsctp(要用 DataChannel 的话)
git clone https://github.com/sctplab/usrsctp.git
cd usrsctp && mkdir build && cd build && cmake .. && make -j && make install
ldconfig && cd ../..

# Janus
git clone https://github.com/meetecho/janus-gateway.git
cd janus-gateway && sh autogen.sh
./configure --prefix=/opt/janus --enable-websockets --enable-data-channels \
  --disable-docs
make -j && make install && make configs

Systemd(/etc/systemd/system/janus.service):

[Unit]
Description=Janus WebRTC Gateway
After=network-online.target

[Service]
User=nobody
Group=nogroup
LimitNOFILE=1048576
ExecStart=/opt/janus/bin/janus -F /opt/janus/etc/janus
Restart=always

[Install]
WantedBy=multi-user.target

4)Janus 关键配置:把“连得上、跑得稳”放第一位

  • 核心文件:/opt/janus/etc/janus/janus.jcfg(核心)
  • 传输:janus.transport.http.jcfg、janus.transport.websockets.jcfg
  • 插件:janus.plugin.videoroom.jcfg

4.1 janus.jcfg(核心片段)

general: {
  configs_folder = "/opt/janus/etc/janus"
  plugins_folder = "/opt/janus/lib/janus/plugins"
  log_to_file = true
  debug_level = 4
}

nat: {
  ice_lite = true
  # 1:1 NAT 或多公网 IP 时启用;单公网主机也建议显式写上,减少候选混乱
  nat_1_1_mapping = "X.X.X.X"   # 这里填你的香港服务器公网 IP
}

media: {
  # WebRTC 媒体的 UDP 端口范围(记得配防火墙)
  rtp_port_range = "20000-24000"
  # 根据实际网卡 MTU 决定是否调低(TURN/TLS 会增加开销)
  # mtu = 1200
}

certificates: {
  # 如你走 WSS/HTTPS 直连而不是 Nginx 反代,则在此放置有效证书
  cert_pem = "/opt/janus/etc/ssl/fullchain.pem"
  cert_key = "/opt/janus/etc/ssl/privkey.pem"
}

提示:nat_1_1_mapping 适合静态公网 IP,会把这个地址加入本地候选,避免内网地址泄露或被选为候选导致 ICE 失败。

4.2 传输层(HTTP/WS)

HTTP(默认 8088/8089),WebSocket(默认 8188/8989)。这是 Janus 的 API/信令接口,课堂业务前端会连它。

janus.transport.websockets.jcfg(片段):

general: {
  ws = true
  ws_port = 8188
  wss = false          # 若走 Nginx 反代,这里可用 ws;由 Nginx 终止 TLS
  # wss = true
  # wss_port = 8989
  # wss_certificates = "/opt/janus/etc/janus/certificates.jcfg"
  pingpong_trigger = 30
  pingpong_timeout = 50
}

Nginx 反代 WSS(推荐,把证书与 TLS 交给 Nginx):

server {
  listen 443 ssl http2;
  server_name janus.example.com;
  ssl_certificate /etc/nginx/ssl/fullchain.pem;
  ssl_certificate_key /etc/nginx/ssl/privkey.pem;

  location /janus {
    proxy_pass http://127.0.0.1:8088/janus;
  }

  location /ws {
    proxy_pass http://127.0.0.1:8188;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 600s;
  }
}

(Nginx 反代 WS/WSS 是常见部署做法,社区里也有大量实践讨论。)

4.3 VideoRoom 插件(SFU 的“心脏”)

janus.plugin.videoroom.jcfg(片段,按互动课优化):

room-10001: {
  description = "Grade-6 Math - HK Edge"
  is_private = false
  publishers = 16                 # 允许同时发流的人数
  bitrate = 1200000               # 每个发布者默认上限
  bitrate_cap = true              # 严格限速,防止突刺拖垮弱网
  fir_freq = 0                    # 不周期性强制关键帧,靠 PLI 触发
  opus_fec = true                 # 音频 FEC
  audiocodec = opus
  videocodec = vp8,vp9,h264       # 客户端按能力选择
  notify_joining = true
}

这些字段(publishers/bitrate/bitrate_cap/fir_freq/opus_fec/...)都来自 VideoRoom 正式文档,那里还写了 simulcast/SVC、rtp_forward 等高级用法。

5)千万别忘 TURN:Janus 是 ICE-Lite,真正需要 TURN 的是客户端

这是很多人踩的坑:你给 Janus 配 TURN 没意义,应该给浏览器客户端配 TURN。官方社区里也多次强调——防火墙/对称 NAT 的场景,用 客户端侧 TURN 才能打通。

5.1 Coturn 安装与配置

apt -y install coturn

/etc/turnserver.conf(要点):

listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
use-auth-secret
static-auth-secret=CHANGE_ME_TO_A_LONG_RANDOM_SECRET
realm=turn.example.com
total-quota=1000
bps-capacity=0
stale-nonce
no-cli
no-multicast-peers
# 公网/私网都写(若有)
external-ip=<PUBLIC_IP>
min-port=30000
max-port=34999
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem

lt-cred-mech + use-auth-secret + static-auth-secret 组合,用 TURN REST 机制为每位学生发放短期凭证(用户名为到期时间戳,密码为 HMAC(username, secret))。Coturn 文档与 IETF 幻灯都有说明。

生成短期凭证(后端示例,Node.js/Python 任一语言都行):

# Python 版
import base64, hmac, hashlib, time
def gen_turn_cred(secret: str, ttl: int = 3600):
    username = str(int(time.time()) + ttl)
    pwd = base64.b64encode(hmac.new(secret.encode(), username.encode(), hashlib.sha1).digest()).decode()
    return username, pwd

前端 Janus 初始化(强制带 TURN):

const iceServers = [{
  urls: [
    "turn:turn.example.com:3478?transport=udp",
    "turns:turn.example.com:5349?transport=tcp"
  ],
  username: TURN_USERNAME,
  credential: TURN_PASSWORD
}];

const janus = new Janus({
  server: "wss://janus.example.com/ws",
  iceServers,
  // 建议打开 trickle,弱网更快收敛
  iceTransportPolicy: "all"
});

6)课堂关键体验优化:simulcast、带宽自适应、缩略图降级

发布端(老师端)强制 simulcast(例如 VP8 三层 180p/360p/720p),学生端订阅时按可视窗口选择层级。Janus 的 VideoRoom 完全支持这套逻辑,带宽一紧,切低层依然稳定说话不卡。

主讲大窗:订 720p;缩略图:订 180p/360p;

移动端:优先低层 + 限帧(15–20fps),保语音优先(Opus FEC 开启)。

动态限速:用 Janus 的 configure API 下发 bitrate 调整,网络劣化时发 PLI 触发关键帧;

WS 心跳:pingpong_trigger/pingpong_timeout 合理调大,防止移动端链路抖动被误判断线。

7)端口与连通性:一条条打通,别想当然

连通性自查清单

wss://janus.example.com/ws(或 HTTP)能握手;

服务器 udp/20000-24000 能被公网直连;

TURN 3478/5349 外网可达,min/max-port 放开;

nat_1_1_mapping 写明公网 IP,避免内网地址被浏览器 ICE 选中导致失败(尤其容器/双网卡)。

8)我真的踩过的坑 & 复盘

现象 根因 处理
晚上 8:10 大量“ICE failed for component 1” 宿主双网卡 + Docker 虚拟网卡,浏览器被送了内网候选 nat_1_1_mapping=公网IP 后问题立刻消失;容器改 host 网络模式更稳(或把接口绑死)
有用户家里路由器防火墙很“毒”,直连 UDP 失败 客户端没带 TURN,或凭证过期 课堂入口强制下发 TURN REST 凭证,有效期 1 小时,过期自动刷新;Coturn 监控 401/438 激增告警
大并发时偶发花屏/卡帧 发布端突发码率 + NACK 洪水 bitrate_cap=true + 缩略层限帧 + 业务层做“单画面放大”而非“订多路高层”
端口段不生效 某版本/编译选项下 rtp_port_range 未正确加载或被覆盖 升级到较新 Janus & libnice,自查日志是否打印端口段;必要时二分回退配置验证(社区也有相关讨论)
WSS 偶发连接失败(自签证书) 浏览器拒绝不受信根 一律走 Nginx 反代并部署正规证书,Janus 仅走 ws/http 内网回环

9)观测与压测:让“体感顺”落到数字上

Janus Admin/Monitor API 打开后,能看到会话、handle、房间、丢包/重传等指标;我用一个小脚本把它们抓到 Prometheus,再在 Grafana 做“房间看板”。

我们一次上线前 A/B(同一批学生):

指标 切 Janus 前(P2P/信令房) 切 Janus 后(香港 SFU + TURN)
平均首帧时间(秒) 3.8 1.9
课堂 30 分钟内掉线率 8.1% 1.7%
视频卡顿(>1s 卡帧/每人·次) 2.6 0.7
主讲平均端到端时延(ms) 230–400 120–220
平均下行带宽/人(Mbps) 2.8 1.6(simulcast 分层起效)

课堂端“顺滑”的主观感受,和首帧时间 + 抖动 + 丢包重传的曲线高度一致。Hong Kong 边缘 + 客户端 TURN,是组合拳。

10)扩容与高可用:多 Janus 分房 + 反亲和

多 Janus 分片:用 L4/L7 网关按房号 Hash 到固定 Janus,避免房内跨机;

远端发布者(房级级联):VideoRoom 支持把本机房发布者远程化到另一台 Janus,实现跨实例“同房订阅”(需要你用 API 驱动),大房/跨地域时非常实用。

Coturn 多边缘:在香港/新加坡各放一台,前端按地理选择最近 TURN。

11)完整“能跑”的最小闭环(你可以直接抄)

  • Ubuntu 安装与内核参数(见 §2)。
  • 编译 Janus(见 §3,务必新 libnice/libsrtp)。
  • 配置 Janus:
  • janus.jcfg: ice_lite=true、nat_1_1_mapping=公网IP、rtp_port_range=20000-24000;
  • websockets.jcfg: 开 ws,配 Nginx 反代做 wss;
  • videoroom.jcfg: 设置 publishers/bitrate/opus_fec/...。
  • Coturn:lt-cred-mech + use-auth-secret + static-auth-secret,开放 3478/5349 与中继端口段,后端发放 TURN REST 短期凭证。
  • 前端:Janus 初始化强制 iceServers 带自家 TURN;发布端开 simulcast;弱网时用 configure 动态降码率。
  • 连通自检:WSS 能连、UDP 20000–24000 可达、TURN 可用(trickle-ice/自检页)、房间首帧 < 2s。
  • 监控:开启 Admin/Monitor,拉核心指标做告警。

上线那晚,香港暴雨,二期教室刚开,客服群里静得出奇。凌晨 2:30 我把风扇往机柜缝里又对了对,合上笔记本。

后来老师跟我说:“同样的网,同样的孩子,今天课堂顺得出奇。”
其实没什么魔法:把媒体面向“弱网”设计,让服务器做该做的转发/拥塞/分层,把“能打通”放在第一优先级,剩下的交给人和时间