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

香港服务器运行 Debian 时,如何用 HLS + WebRTC 混合架构把直播延迟打下来

发布人:Minchunlin 发布时间:2025-09-23 09:14 阅读量:1055


夜里 1:40,效果葵涌机房 9 楼的机柜温度刚被拉回 26℃,新接入的国庆专场直播还在压测。白天产品一句“要把直播延迟做到观众感知不到”,把我从运维切成了半个多媒体工程师。前两版只用 HLS,3~6 秒延迟无论怎么切片都下不去;全上 WebRTC,规模一扩大就喊硬件顶不住。

第三版,我决定折中:入口推流统一,源站用一套媒体服务器同时出 WebRTC 与 LL-HLS,边缘用 Nginx 做 HLS 的大规模缓存,WebRTC 只服务“真·低延迟”人群。这篇手记,就是我在香港 Debian 环境把它跑通、优化、踩坑、救火的完整过程。

目标与设计取舍

目标延迟:

  • WebRTC:端到端 <= 800 ms(香港本地 300~600 ms 常见;跨境 500~900 ms)。
  • LL-HLS:端到端 1.5~3.0 s(观感接近“准实时”,便于 CDN/缓存扩散)。

取舍:

  • WebRTC 负责“主播连麦、互动答题、带货秒杀”这类对时延极敏感的观众;
  • LL-HLS 覆盖 80%+ 普通观看用户,换来更稳的扩展性与成本。

1. 架构一览

[OBS/FFmpeg 推流]
         │ RTMP/SRT
         ▼
   ┌──────────────────────┐
   │  Origin 媒体源站 (OvenMediaEngine, Debian 12) │
   │  • 接收:RTMP/SRT                              │
   │  • 输出:WebRTC(WHIP/WHEP)、LL-HLS(CMAF, PART) │
   └───────────┬───────────┘
               │
     HTTPS/UDP │
               ▼
   ┌──────────────────────┐             ┌──────────────────────┐
   │  Edge Nginx (多台)   │ <———>  │  TURN (coturn, TLS 443)│
   │  • 缓存 HLS/CMAF      │         │  WebRTC 备援穿透     │
   └──────────────────────┘             └──────────────────────┘
               │
         Web/WSS/WHEP
               ▼
        [浏览器/移动端]
        • 优先 WebRTC,不通则切到 LL-HLS

关键点:统一源站编码(GOP 对齐、码率阶梯一致),边缘只缓存 HLS;WebRTC 不走 CDN,依靠源站+TURN 做穿透与小规模并发。

2. 现场硬件与网络清单(真实可复用)

角色 机型/配置 OS 网络 存储
Origin 源站 2× Intel Xeon Silver 4310(20C/40T),64GB RAM,NVMe 1TB×2 Debian 12 (bookworm) 2×10GbE(Bond, LACP),BGP 多线(HKIX+CN2) NVMe RAID1,ext4
Edge 缓存(×3) AMD EPYC 7302P(16C/32T),64GB RAM Debian 12 10GbE NVMe 2TB(proxy_cache)
TURN 4C/8G Debian 12 1GbE(独立公网 /29) SSD 100G
监控 4C/8G Debian 12 1GbE SSD 100G

备注:GPU 非必需。如需服务端转码(ABR 多码率),我在第二阶段加了一张 NVIDIA T4(NVENC),把 WebRTC 侧的 B 帧去掉,使编码时延更稳。

3. 端口与协议规划

组件 端口 协议 用途
Nginx/Edge 80/443 TCP/HTTP1.1/2 HLS/LL-HLS 播放、静态页、API
OME 源站 1935 TCP RTMP 推流
OME 源站 3333/3334 TCP(HTTPS) WHIP/WHEP(WebRTC 信令 HTTP)
OME 源站 10000-10100 UDP WebRTC RTP/RTCP 媒体端口池
coturn 3478/5349 UDP/TCP STUN/TURN(5349 走 TLS)

4. 操作系统准备(Debian 12)

4.1 基础与内核参数

apt update && apt -y upgrade
apt -y install curl git vim htop jq net-tools iftop iperf3 nftables chrony

# 文件句柄与进程数
cat >> /etc/security/limits.conf <<'EOF'
* soft nofile 1048576
* hard nofile 1048576
* soft nproc  65535
* hard nproc  65535
EOF

# sysctl:UDP/TCP 缓冲、BBR、队列
cat >/etc/sysctl.d/99-live-lowlatency.conf <<'EOF'
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.netdev_max_backlog = 250000
net.ipv4.udp_mem = 8388608 12582912 16777216
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
net.ipv4.ip_local_port_range = 10000 65535
fs.file-max = 2097152
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_mtu_probing = 1
EOF
sysctl --system

# 时钟同步(毫秒级)
systemctl enable --now chrony
chronyc sources -v

4.2 防火墙(nftables)

cat >/etc/nftables.conf <<'EOF'
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority 0;
    ct state established,related accept
    iif lo accept

    tcp dport {22} accept
    tcp dport {80,443,1935,3333,3334} accept
    udp dport {3478,5349,10000-10100} accept

    icmp type echo-request accept
    counter drop
  }
}
EOF
systemctl enable --now nftables

5. 软件栈与目录

媒体源站:OvenMediaEngine(简称 OME),统一出 WebRTC + LL-HLS。

边缘缓存:Nginx(open_file_cache + proxy_cache,支持 HTTP/2)。

穿透备援:coturn(TLS 5349,走 443 端口可穿绝大多数网络)。

编解码:客户端 OBS/FFmpeg 推流;如需服务端转码再启用 NVENC。

我选 OME 的原因:一套进程同时支撑 WebRTC 与 LL-HLS(CMAF + PART),信令基于 WHIP/WHEP,用起来比“多套拼接”更省心。

本文示例使用 Docker Compose 部署,保持可移植、可回滚。

apt -y install docker.io docker-compose-plugin
mkdir -p /opt/live/{ome,nginx,turn,cert,log}

6. 部署 OME(WebRTC + LL-HLS)

6.1 目录与证书

# 证书可先占位,实际用 acme.sh/lego 申请
ls /opt/live/cert/
# fullchain.pem  privkey.pem

6.2 docker-compose.yml

version: "3.9"
services:
  ome:
    image: airensoft/ovenmediaengine:latest
    container_name: ome
    network_mode: host  # 省心处理 UDP 端口
    volumes:
      - /opt/live/ome/conf:/opt/ovenmediaengine/bin/origin_conf
      - /opt/live/cert:/opt/ovenmediaengine/bin/cert
      - /opt/live/log:/opt/ovenmediaengine/bin/log
    restart: unless-stopped

我用 host 网络是为了不被 Docker 的 NAT 干扰 WebRTC 的 UDP 端口映射。如果你更熟悉 iptables/nft,也可以桥接模式手动放通。

6.3 OME 配置(/opt/live/ome/conf/Server.xml)

注意:下面配置聚焦关键段落,路径与端口需与你的环境一致。

<Server>
  <Bind>
    <Address>0.0.0.0</Address>
    <Port>3334</Port> <!-- HTTPS for WHEP/WHIP -->
    <TLSPort>3334</TLSPort>
    <CertFile>/opt/ovenmediaengine/bin/cert/fullchain.pem</CertFile>
    <KeyFile>/opt/ovenmediaengine/bin/cert/privkey.pem</KeyFile>
  </Bind>

  <VirtualHosts>
    <VirtualHost>
      <Name>live.example.hk</Name>
      <Applications>
        <Application>
          <Name>live</Name>
          <Type>live</Type>

          <!-- 输入:RTMP 推流,亦可加 SRT -->
          <Inputs>
            <Rtmp>
              <Enable>true</Enable>
              <Port>1935</Port>
            </Rtmp>
          </Inputs>

          <!-- 输出 1:WebRTC(WHEP 播放),Opus/AAC + H264,无 B 帧 -->
          <Outputs>
            <WebRTC>
              <Enable>true</Enable>
              <RTP>
                <PortRange>10000-10100</PortRange>
              </RTP>
              <Codec>
                <Video>
                  <H264>
                    <Profile>baseline</Profile>
                    <GOP>2</GOP> <!-- 2s GOP -->
                    <BFrames>0</BFrames>
                  </H264>
                </Video>
                <Audio>
                  <Opus>
                    <Bitrate>128000</Bitrate>
                  </Opus>
                </Audio>
              </Codec>
              <StunServer>stun:turn.example.hk:3478</StunServer>
              <TurnServer>turns:turn.example.hk:5349?transport=tcp</TurnServer>
              <TurnUsername>webrtc</TurnUsername>
              <TurnPassword>STRONG_SECRET</TurnPassword>
            </WebRTC>

            <!-- 输出 2:LL-HLS(CMAF, PART 400ms)-->
            <LLHls>
              <Enable>true</Enable>
              <SegmentDuration>2</SegmentDuration>
              <PartDuration>0.4</PartDuration>
              <PlaylistLength>6</PlaylistLength>
              <Cmaf>true</Cmaf>
              <Path>/var/www/hls/live</Path>
              <CORS>*</CORS>
            </LLHls>
          </Outputs>

          <!-- 可选:ABR 多码率(服务端转码)-->
          <Transcoder>
            <Enable>false</Enable>
            <!-- 先关掉,后文演示启用 NVENC -->
          </Transcoder>

        </Application>
      </Applications>
    </VirtualHost>
  </VirtualHosts>
</Server>

经验:WebRTC 端编码选 baseline/zerolatency,GOP=1~2s,B 帧=0,能稳定把端到端时延压在 1s 内。

启动:

docker compose up -d
journalctl -u docker -f | grep oven

7. 部署 Nginx Edge(缓存 LL-HLS)

7.1 Nginx 配置

/opt/live/nginx/nginx.conf

worker_processes auto;
events { worker_connections  10240; use epoll; multi_accept on; }

http {
  sendfile on; tcp_nopush on; tcp_nodelay on; aio threads; directio 4m;
  keepalive_timeout  30; types_hash_max_size 2048; server_tokens off;
  proxy_buffering on; proxy_request_buffering off; proxy_http_version 1.1;
  proxy_set_header Connection ""; # 允许分块传输

  # 缓存与文件句柄
  open_file_cache max=100000 inactive=60s;
  open_file_cache_valid 120s;
  open_file_cache_min_uses 2;
  open_file_cache_errors on;

  proxy_cache_path /var/cache/nginx/hls levels=1:2 keys_zone=hls:2g
                   max_size=150g inactive=10m use_temp_path=off;

  map $http_origin $cors {
    default "*";  
  }

  server {
    listen 80;
    server_name live.example.hk;
    return 301 https://$host$request_uri;
  }

  server {
    listen 443 ssl http2;
    server_name live.example.hk;
    ssl_certificate     /opt/live/cert/fullchain.pem;
    ssl_certificate_key /opt/live/cert/privkey.pem;

    # CORS for HLS
    add_header Access-Control-Allow-Origin $cors always;
    add_header Access-Control-Allow-Methods 'GET, OPTIONS' always;

    location /hls/ {
      proxy_cache hls;
      proxy_cache_valid 200 302 10m;
      proxy_cache_valid 404 1m;
      proxy_buffer_size 512k;
      proxy_buffers 64 512k;
      proxy_busy_buffers_size 512k;
      proxy_temp_file_write_size 512k;

      proxy_pass http://127.0.0.1:8080/hls/; # 由 OME/或本地静态服务出 HLS
    }

    # 反向代理 WHEP/WHIP 到 OME (HTTPS)
    location /whep/ {
      proxy_pass https://127.0.0.1:3334/whep/;
    }
    location /whip/ {
      proxy_pass https://127.0.0.1:3334/whip/;
    }
  }
}

说明:我把 OME 生成的 LL-HLS 输出目录挂成本机 8080(可以用 caddy 或 nginx 再起一个本地 server),Edge 只负责公共 443 的缓存与转发。

本地出 HLS(静态)示例:

server {
  listen 8080;
  location /hls/ { root /var/www; }
}

8. 部署 TURN(coturn)

apt -y install coturn
sed -i 's/^#TURNSERVER_ENABLED=.*/TURNSERVER_ENABLED=1/' /etc/default/coturn

cat >/etc/turnserver.conf <<'EOF'
listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
realm=turn.example.hk
server-name=turn.example.hk
use-auth-secret
static-auth-secret=STRONG_SECRET
total-quota=0
bps-capacity=0
stale-nonce
cert=/opt/live/cert/fullchain.pem
pkey=/opt/live/cert/privkey.pem
no-multicast-peers
no-cli
no-tlsv1
no-tlsv1_1
cipher-list=TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
min-port=10000
max-port=10100
EOF

systemctl enable --now coturn
journalctl -u coturn -f

经验:跨境链路经常对 UDP 不友好,5349/TCP+TLS 的 TURN 中继是“兜底选项”,哪怕牺牲一些带宽,也比“无法播放”强。

9. 推流与播放

9.1 OBS/FFmpeg 推流(RTMP)

编码建议(主播端):

视频:H.264, 30fps, GOP=60(2s),Tune zerolatency,Profile baseline/main,CBR 3~5 Mbps;

音频:AAC 128 kbps 或 Opus 96~128 kbps;

关键帧间隔与服务器保持一致(2s),码率阶梯与 ABR 配置对齐。

FFmpeg 推流样例:

ffmpeg -re -stream_loop -1 -i demo.mp4 \
  -c:v libx264 -preset veryfast -tune zerolatency -x264-params keyint=60:min-keyint=60:scenecut=0 \
  -b:v 4500k -maxrate 4500k -bufsize 9000k \
  -c:a aac -b:a 128k -ar 48000 -ac 2 \
  -f flv rtmp://live.example.hk:1935/live/room001

9.2 前端播放器策略(伪代码)

优先 WebRTC(WHEP),失败或网络不支持则自动降级 HLS。

HLS 使用 hls.js,开启 lowLatencyMode:true,代理 LL-HLS(CMAF, PART)。

<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="v" playsinline autoplay muted></video>
<script>
(async () => {
  const v = document.getElementById('v');

  // 1) Try WebRTC via WHEP
  try {
    const offer = await createWhepOffer();
    await fetch('https://live.example.hk/whep/live/room001', {
      method: 'POST', headers: {'Content-Type': 'application/sdp'}, body: offer.sdp
    }).then(r => r.text()).then(answer => setRemote(answer));
    return; // WebRTC OK
  } catch (e) { console.warn('WHEP failed, fallback to HLS', e); }

  // 2) Fallback to LL-HLS
  const url = 'https://live.example.hk/hls/room001/index.m3u8';
  if (Hls.isSupported()) {
    const hls = new Hls({lowLatencyMode:true, backBufferLength:30});
    hls.loadSource(url); hls.attachMedia(v);
  } else { v.src = url; v.play(); }
})();
</script>

说明:createWhepOffer()/setRemote() 需要你用原生 WebRTC API 生成 SDP,这里略去细节。多数现代媒体服务器(如 OME)已经内置 WHEP 端点。

10. 服务端转码与码率阶梯(可选)

当主播端条件不一时,我在源站启用服务端转码,生成 ABR:1080p/6M、720p/3.5M、480p/1.8M。启用 NVENC 后,CPU 压力显著下降。

Server.xml 片段:

<Transcoder>
  <Enable>true</Enable>
  <VideoProfiles>
    <Profile name="1080p">-c:v h264_nvenc -preset p5 -b:v 6000k -maxrate 6000k -bufsize 12000k -g 60 -bf 0</Profile>
    <Profile name="720p"> -c:v h264_nvenc -preset p5 -b:v 3500k -maxrate 3500k -bufsize 7000k -g 60 -bf 0</Profile>
    <Profile name="480p"> -c:v h264_nvenc -preset p6 -b:v 1800k -maxrate 1800k -bufsize 3600k -g 60 -bf 0</Profile>
  </VideoProfiles>
  <AudioProfiles>
    <Profile name="opus128">-c:a libopus -b:a 128k -ar 48000</Profile>
  </AudioProfiles>
  <Profiles>
    <Profile name="1080p_opus" video="1080p" audio="opus128" />
    <Profile name="720p_opus"  video="720p"  audio="opus128" />
    <Profile name="480p_opus"  video="480p"  audio="opus128" />
  </Profiles>
</Transcoder>

11. 监控与告警

节点监控:node_exporter + Prometheus + Grafana;

关键指标:UDP 丢包率、RTP 抖动、编码耗时、转码队列长度、Nginx 命中率、磁盘 IO 延迟、TCP RTT 分布;

简单速查:

# UDP 丢包(内核)
sudo ethtool -S eth0 | egrep 'rx_*_errors|tx_*_errors'
# RTP 抖动(抓包)
sudo tcpdump -i any udp portrange 10000-10100 -vv -G 10 -W 1 -w rtp.pcap

12. 优化清单(踩过坑后我保留的“保命线”)

  • 编码端:B 帧=0,GOP=2s,tune=zerolatency;音频尽量用 Opus。
  • 内核与队列:bbr + fq 组合、netdev_max_backlog=250000、增大 rmem/wmem。
  • Nginx:proxy_request_buffering off + Connection "" 支持 LL-HLS 部分分片的分块传输;开启 open_file_cache;HLS 目录挂 NVMe。
  • TURN:强制 TLS 5349,端口范围与 OME 对齐(10000-10100)。
  • 证书与 OCSP:启用 OCSP Stapling,避免边缘偶发长握手导致“卡进度条”。
  • 时钟:源站、边缘、客户端尽量与同一 NTP 源对齐,避免 EXT-X-PROGRAM-DATE-TIME 乱序。
  • 路由:香港 BGP 多线(HKIX + CN2)显著降低跨境抖动;观众主要在内地就尽量拉 CN2/CMI 优化段。
  • 冷热分层:HLS 最近 2~3 分钟热段走 NVMe,老段下刷到 SSD/SATA,节省贵价 NVMe。
  • ABR 切换:关键帧对齐;分辨率/码率阶梯之间码率相差 1.7~2.2×,减少来回震荡。

13. 实测数据(节选)

压测方法:本地(湾仔)与内地(深圳/上海)各 50 人混合,40 HLS + 10 WebRTC,播放 30 分钟。

路径 类型 平均时延 95 分位 丢包(上/下) 备注
HK 本地 WebRTC 420 ms 680 ms 0.2% / 0.3% TURN 不启用
HK→深圳 WebRTC 610 ms 920 ms 0.6% / 0.9% 偶发走 TURN 5349
HK 本地 LL-HLS 1.8 s 2.4 s PART 400 ms,Playlist 6
HK→上海 LL-HLS 2.3 s 3.1 s CDN 边缘命中 92%

14. 常见故障与现场处置

浏览器能看 HLS,看不了 WebRTC:

  • 检查 10000-10100 UDP 是否放通;
  • 运营商丢 UDP?强制 WHEP 使用 turns://(TCP+TLS 5349);
  • 证书域名与 Origin 不一致会被浏览器拒绝。

HLS 卡白屏:

  • Nginx 未设置 proxy_request_buffering off 导致分块延迟;
  • OME 的 PART 时长太短(如 200ms)在弱网下更容易抖动,上调到 400~500ms 反而稳。

跨境观众“声音领先”:

  • 观众侧 HLS 与 WebRTC 混切造成的感知差异,统一锁定为 HLS 或 WebRTC;
  • 检查 CMAF Timebase 与 EXT-X-PROGRAM-DATE-TIME。

CPU 飙高:

  • 服务端转码开太多档位;
  • NVENC 未启用/驱动异常;
  • Edge Nginx 的 proxy_cache 未命中,所有人都在打源站。

TLS 握手很慢:

  • 开启 OCSP Stapling;
  • 减少证书链冗余;
  • 合理使用 HTTP/2 复用。

凌晨 3:05,我在机房走廊的自动售卖机前,插着口袋里的万能表吃掉了当晚的第二根士力架。微信群里最后一个“卡顿”的反馈消失在 2:58。WebRTC 的并发被我们严控在互动区间,LL-HLS 托住了大盘用户,“低延迟”终于不是 PPT 里的形容词。这套混合架构并不“完美”,但足够可控、可扩、可救。

后来几个活动,我没再通宵。你要问“彻底解决低延迟直播传输问题”有没有银弹?我会说:没有银弹,只有清晰的分层与边界。知道该让谁走 WebRTC、让谁走 HLS,知道每一层出问题时你能在哪里把它捞回来——这,就够了。

A. 附录:实用脚本片段

acme.sh 申请证书(Cloudflare DNS 验证示例)

curl https://get.acme.sh | sh
export CF_Token=xxxx
~/.acme.sh/acme.sh --issue --dns dns_cf -d live.example.hk \
  --keylength ec-256 --server letsencrypt
~/.acme.sh/acme.sh --install-cert -d live.example.hk \
  --ecc --fullchain-file /opt/live/cert/fullchain.pem \
  --key-file /opt/live/cert/privkey.pem --reloadcmd "systemctl reload nginx || true"

Nginx HLS 目录权限

mkdir -p /var/www/hls && chown -R www-data:www-data /var/www
mkdir -p /var/cache/nginx/hls && chown -R www-data:www-data /var/cache/nginx/hls

快速链路测试

iperf3 -s   # 源站
iperf3 -c live.example.hk -u -b 20M -t 30  # 观众端 UDP
目录结构
全文