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

大促直播抗压:如何在香港服务器集群里落地 QUIC/HTTP/3、GOP 设计与转码节点调度

发布人:Minchunlin 发布时间:2025-09-26 10:31 阅读量:791


大促前一晚 01:40,直播主会场已经在香港集群预热,第一批种草场次涌入量比我们压测高出 18%。告警一片绿,唯独边缘节点的 HTTP/3 丢包率飙到了 7%——是 UDP 被清洗设备“关照”了。更糟的是,我们的 GOP 设置在某些路由段下变得“易碎”,观众端频繁黑屏回退。

那一刻我做了三件事:

  • 先切生产权重,把 HTTP/2 备路提到 25%;
  • 现场把边缘 Nginx 的 QUIC reuseport worker 数提到 8,放宽内核 UDP 缓冲;
  • 把转码队列策略从“最短队列优先”改成“NVENC 会话+显存双约束的装箱调度”。

15 分钟后,黑屏消失、卡顿告警从红转黄,再到绿。第二天的主场,我们抗住了峰值 4.3 倍的瞬时并发。

这篇文章,把我在香港机房这套 QUIC/HTTP/3 + 稳健 GOP + 转码节点调度 的搭建与优化,完整、细致地复盘给你。

1. 架构总览(拓扑与职责)

主播端(SRT/RTMP) → 香港入口(ingest) → 转码集群(GPU) → 打包(CMAF/LL-HLS, DASH)
     │                                            │
     └────────── 监控/调度队列(Redis/NATS) ───────┘
                                   │
              香港边缘(HTTP/3 Edge, Nginx QUIC) ←→ 观众(移动/宽带)
                                   │
                         备路(HTTP/2/HTTPS, CDN联邦)

 

  • ingest 节点:SRT/RTMP 接入,做鉴权与首跳缓冲(SRS 或 Nginx-RTMP)。
  • 转码集群:GPU(NVIDIA L4/T4/A10 混部),FFmpeg NVENC 多码率输出。
  • 打包节点:Shaka Packager / FFmpeg HLS/DASH(CMAF + 2s 段,LL-HLS 开启 preload hints)。
  • 边缘节点:Nginx QUIC/HTTP/3 + 大缓存盘,代理打包节点;提供 Alt-Svc,HTTP/2 备路。
  • 控制平面:调度队列 + Prometheus + Alertmanager + Grafana。
  • 网络:香港机房双线接入(HGC + PCCW),BGP(ECMP),L4 清洗前置,UDP 白名单。

2. 服务器与网络选型(“够用且抗波动”)

角色 机型/CPU GPU 内存 磁盘 网卡 备注
ingest EPYC 7302P(16c) - 64GB NVMe 960GB 2×25GbE SRT/RTMP,鉴权与缓冲
转码-GPU EPYC 7513(32c) NVIDIA L4 24GB ×2(混有 T4×2 机) 128GB NVMe 1.92TB 2×25GbE NVENC 主力,功耗友好
打包 Xeon 6338N(32c) - 128GB NVMe 3.84TB 2×25GbE CMAF/LL-HLS/DASH
边缘-HTTP/3 EPYC 7502P(32c) - 128GB NVMe 7.68TB ×2 (RAID1) 2×25GbE Nginx QUIC + 大缓存
路由 AS5812-54X - - - 48×10/25 + 6×100G BGP/ECMP,sFlow

容量经验值(我线下测得,仅供参考):

  • 每张 L4 稳定跑 1080p30 → 6 档 ABR(见 §6)≈ 18–24 路;
  • T4 保守按 10–14 路;
  • 打包节点 32c 能扛 6–8 Gbps CMAF I/O;
  • 边缘单机缓存 >10TB 能覆盖 10–20 分钟“热点窗”。

3. 系统与内核调优(UDP/QUIC 的地基)

/etc/sysctl.d/99-quic.conf(核心摘录)

# UDP 缓冲与队列
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.udp_rmem_min = 131072
net.ipv4.udp_wmem_min = 131072
net.core.netdev_max_backlog = 250000
net.core.somaxconn = 65535

# 端口与conntrack(边缘/网关)
net.ipv4.ip_local_port_range = 10000 65535
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_udp_timeout = 60
net.netfilter.nf_conntrack_udp_timeout_stream = 180

# RPS/XPS 与多队列(需要 ethtool 配合)
net.core.rps_sock_flow_entries = 32768

# BBR 作为 TCP 备路拥塞算法(HTTP/2 fallback)
net.ipv4.tcp_congestion_control = bbr

nftables 允许 UDP/443 并做基础限速(防突刺)

add table inet filter
add chain inet filter input { type filter hook input priority 0; policy drop; }
add rule inet filter input ct state established,related accept
add rule inet filter input iifname "eth0" udp dport 443 meter h3 { ip saddr limit rate over 2000/second } counter drop
add rule inet filter input iifname "eth0" udp dport 443 accept
add rule inet filter input tcp dport {80,443} accept
add rule inet filter input icmp type echo-request limit rate 50/second accept
add rule inet filter input ip protocol udp drop

坑点:清洗/防火墙默认丢 UDP 443。上线前务必打通:ISP → 清洗 → 防火墙 → 边缘,逐段抓包。边缘机上 ethtool -S 查看 NIC 丢包计数是否增长,必要时调大 RX 队列与 IRQ 亲和。

4. QUIC/HTTP/3 边缘部署(Nginx 主路,HTTP/2 备路)

Nginx(1.25+ 主线支持 QUIC)配置 nginx.conf(关键段)

worker_processes auto;
events { worker_connections  65535; }

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile      on;
    aio           threads;
    tcp_nopush    on;

    # 缓存区
    proxy_cache_path /data/cache levels=1:2 keys_zone=hls:10g
                      max_size=200g inactive=30m use_temp_path=off;
    proxy_buffers 256 16k;
    proxy_busy_buffers_size 64k;

    map $http_upgrade $connection_upgrade { default upgrade; '' close; }

    server {
        # QUIC/HTTP3 主路 + HTTP/2 备路
        listen 443 quic reuseport;
        listen 443 ssl http2;

        server_name edge.hk.example.com;
        ssl_certificate     /etc/letsencrypt/live/edge/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/edge/privkey.pem;
        ssl_protocols TLSv1.3;
        ssl_early_data off;

        # H3 宣告
        add_header Alt-Svc 'h3=":443"; ma=86400' always;
        add_header X-QUIC $quic always;

        # HLS/DASH 缓存策略
        location ~* \.(m3u8|mpd)$ {
            proxy_pass http://packager.internal;
            proxy_set_header Host $host;
            proxy_cache hls;
            proxy_cache_valid 200 206 1s;         # 清单短缓存
            proxy_ignore_headers Cache-Control Expires;
            add_header Cache-Status $upstream_cache_status always;
        }

        location ~* \.(m4s|cmf|ts|mp4)$ {
            slice 1m;                              # Range 友好
            proxy_set_header Range $slice_range;
            proxy_pass http://packager.internal;
            proxy_cache hls;
            proxy_cache_valid 200 206 10m;
            add_header Cache-Status $upstream_cache_status always;
        }
    }

    upstream packager.internal {
        server 10.10.20.11:8080 max_fails=3 fail_timeout=10s;
        server 10.10.20.12:8080 max_fails=3 fail_timeout=10s;
        keepalive 64;
    }
}

经验:H3 客户端一旦命中 Alt-Svc 就极难回退。所以务必保留 listen 443 ssl http2 备路,并通过 权重调度 控制新客户端迁移节奏(灰度)。

5. 入口 ingest:SRT/RTMP 混接与首跳稳定

我选择 SRS 做接入(OBS 推流同学也熟)。基础配置片段:

listen              1935;           # RTMP
srt_server {
    enabled         on;
    listen          9000;
    maxbw           100000000;      # 100Mbps
    latency         40;             # 40ms * 2 = 80ms 端到端
}
vhost __defaultVhost__ {
    ingestion {
        allow publish 10.0.0.0/8 172.16.0.0/12;  # 上游推流白名单
    }
    dvr {
        enabled     on;
        dvr_path    /data/dvr/[app]/[stream]/[timestamp].flv;
    }
    http_hooks {
        # 鉴权与开关流
        on_publish      http://ctrl/auth/on_publish;
        on_unpublish    http://ctrl/auth/on_unpublish;
    }
    transcode {                                # 可选:轻量预处理
        ffmpeg /usr/bin/ffmpeg;
        engine preview {
            enabled     on;
            vfilter     scale=640:-1;
            vcodec      libx264;
            vbitrate    500;
        }
    }
}

线上小技巧:SRT latency 不宜太小,我最终落在 80–120ms;RTMP 保留做兜底接入。

6. 编码与 GOP 设计(“易传、易播、易缓存”的三易原则)

ABR 瀑布(CMAF/H.264 主线,HEVC 可选)

档位 分辨率 视频码率 音频 GOP(秒) 关键参数
0 2160p30 12000 kbps 128 kbps 2 -g 60 -keyint_min 60 -sc_threshold 0 -bf 2 -maxrate 14M -bufsize 24M
1 1440p30 8000 kbps 128 kbps 2 同上,按档位等比缩放
2 1080p30 5000 kbps 128 kbps 2  
3 720p30 3500 kbps 128 kbps 2  
4 540p30 2000 kbps 96 kbps 2  
5 360p30 1000 kbps 96 kbps 2  

GOP 固定 2s,所有档位对齐,利于低延迟、快速切档与缓存命中。

不用场景切关键帧(-sc_threshold 0),避免网络抖动时产生“软分片”。

B 帧=2,NVENC 设置 -bf 2 -b_ref_mode middle -temporal-aq 1,在移动端兼容性与压缩率间取平衡。

FFmpeg NVENC 多码率示例(L4/T4 通用)

ffmpeg -y -hide_banner -hwaccel cuda -hwaccel_output_format cuda \
  -i "srt://0.0.0.0:9000?mode=listener&latency=80" \
  -filter_complex "\
    [0:v]split=6[v2160][v1440][v1080][v720][v540][v360];
    [v2160]scale_cuda=w=3840:h=2160:flags=bicubic,format=nv12[v2160o];
    [v1440]scale_cuda=w=2560:h=1440:flags=bicubic,format=nv12[v1440o];
    [v1080]scale_cuda=w=1920:h=1080:flags=bicubic,format=nv12[v1080o];
    [v720] scale_cuda=w=1280:h=720: flags=bicubic,format=nv12[v720o];
    [v540] scale_cuda=w=960:h=540:  flags=bicubic,format=nv12[v540o];
    [v360] scale_cuda=w=640:h=360:  flags=bicubic,format=nv12[v360o];
    [0:a]aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo[aout]" \
  -map "[v2160o]" -c:v h264_nvenc -preset p5 -profile:v high -b:v 12M -maxrate 14M -bufsize 24M -g 60 -keyint_min 60 -bf 2 -b_ref_mode middle -rc:v vbr -spatial-aq 1 -temporal-aq 1 \
  -map "[v1440o]" -c:v h264_nvenc -b:v 8M  -maxrate 9M  -bufsize 16M -g 60 -keyint_min 60 -bf 2 -b_ref_mode middle -rc:v vbr -spatial-aq 1 -temporal-aq 1 \
  -map "[v1080o]" -c:v h264_nvenc -b:v 5M  -maxrate 6M  -bufsize 12M -g 60 -keyint_min 60 -bf 2 -b_ref_mode middle -rc:v vbr -spatial-aq 1 -temporal-aq 1 \
  -map "[v720o]"  -c:v h264_nvenc -b:v 3.5M -maxrate 4.2M -bufsize 7M  -g 60 -keyint_min 60 -bf 2 -b_ref_mode middle -rc:v vbr -spatial-aq 1 -temporal-aq 1 \
  -map "[v540o]"  -c:v h264_nvenc -b:v 2M   -maxrate 2.4M -bufsize 4M  -g 60 -keyint_min 60 -bf 2 -b_ref_mode middle -rc:v vbr -spatial-aq 1 -temporal-aq 1 \
  -map "[v360o]"  -c:v h264_nvenc -b:v 1M   -maxrate 1.2M -bufsize 2M  -g 60 -keyint_min 60 -bf 2 -b_ref_mode middle -rc:v vbr -spatial-aq 1 -temporal-aq 1 \
  -map "[aout]" -c:a aac -b:a 128k -ac 2 -ar 48000 -preset medium \
  -f hls -hls_time 2 -hls_list_size 6 -hls_flags independent_segments+delete_segments+program_date_time \
  -master_pl_name master.m3u8 -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0 v:3,a:0 v:4,a:0 v:5,a:0" \
  /var/www/hls/out_%v/index.m3u8

小坑:如果你把 -g 设为 2s,而摄像端是 25fps,务必用 50 帧 GOP(或者按 30fps = 60)。GOP 不对齐,HLS/LL-HLS 在弱网下更容易断档。

7. 打包:CMAF + LL-HLS/DASH

我在打包端选 Shaka Packager 保持 CMAF 一致性,FFmpeg 也可。

Shaka Packager 示例(2s 段)

packager \
  in=udp://127.0.0.1:10000?interface=10.10.30.10,stream=audio,init_segment=out/audio_init.mp4,segment_template=out/audio_$Number$.m4s \
  in=udp://127.0.0.1:10001?interface=10.10.30.10,stream=video,init_segment=out/1080p_init.mp4,segment_template=out/1080p_$Number$.m4s,bandwidth=5000000 \
  --hls_master_playlist_output out/master.m3u8 \
  --mpd_output out/stream.mpd \
  --segment_duration 2 \
  --low_latency_dash_mode \
  --suggested_presentation_delay 2

LL-HLS 关键点:

  • 开启 preload-hint 与 PART;
  • 边缘缓存对 .m3u8 仅短缓存(1s),对 .m4s/.ts 长缓存(10m)。

8. 转码节点调度(“NVENC 会话 + 显存 + IO” 三约束装箱)

我用 Kubernetes + 自研调度器(基于队列),GPU 资源靠官方 nvidia-device-plugin 暴露。

节点标注

kubectl label node gpu-01 accelerator=nvidia-l4 gpu.mem=24Gi nvenc.sessions=48
kubectl label node gpu-02 accelerator=nvidia-t4 gpu.mem=16Gi nvenc.sessions=24

Worker 部署(每 Pod 占 1 GPU)

apiVersion: apps/v1
kind: Deployment
metadata: { name: transcoder-worker }
spec:
  replicas: 8
  selector: { matchLabels: { app: transcoder } }
  template:
    metadata: { labels: { app: transcoder } }
    spec:
      nodeSelector: { accelerator: nvidia-l4 }
      containers:
      - name: ffmpeg
        image: ghcr.io/acme/ffmpeg-nvenc:6.1
        resources:
          limits:
            nvidia.com/gpu: 1
            cpu: "6"
            memory: "8Gi"
        env:
        - { name: QUEUE_BROKER, value: "redis://scheduler:6379/0" }
        - { name: MAX_SESSIONS, value: "18" }   # L4上我实测稳态值
        - { name: WATERMARK_MBPS, value: "140" } # 每GPU输出带宽水位

调度器(Python 伪码):按 NVENC 会话数 + 显存 + 预估 Mbps 进行装箱,优先放入“最紧凑仍不溢出”的节点,避免冷热不均。

def place(job, nodes):
    # job: { sessions:6, vram:4, mbps:20 }
    candidates = []
    for n in nodes:
        if n.sess_free >= job.sessions and n.vram_free >= job.vram and (n.mbps + job.mbps) <= n.mbps_cap:
            waste = min(n.sess_free - job.sessions, n.vram_free - job.vram, n.mbps_cap - (n.mbps+job.mbps))
            candidates.append((waste, n))
    return sorted(candidates, key=lambda x: x[0])[0][1] if candidates else None

伸缩:

  • HPA 指标 transcode_queue_depth > 50 触发扩副本;
  • VPA 仅建议(不自动),避免显存抖动;
  • 高峰前 30 分钟预热 30% 余量,避免冷起导致首秒掉帧。

9. 边缘缓存与回源优化

分片缓存:slice 1m 配合 Range,大幅提高长尾命中;

清单短缓存:proxy_cache_valid 1s,但配合 ETag/Last-Modified,让客户端聪明;

回源连接池:keepalive 64,并开启 http2 回源以降低头开销;

熔断回退:当 H3 RTT>120ms 或丢包>5%,用业务网关将该 ASN 的 Alt-Svc 权重降低,强制更多客户端走 H2。

10. 压测与观测

HTTP/3 客户端压测(nghttp3 的 h3load)

h3load -n 500000 -c 500 -t 32 https://edge.hk.example.com/live/room1/master.m3u8
# 建议实际压测命中片段URL而非仅master:脚本随机取 m4s/ts 列表并并发请求

关键信号(Grafana 面板我常放这几项):

  • node_netstat_Udp_InErrors(UDP 错误),netdev_rx_dropped(网卡丢)
  • Nginx $quic 指标分布(握手失败 / 0RTT 命中率)
  • 边缘 Cache 命中率(m3u8 vs m4s 分开看)
  • 转码队列深度、每 GPU NVENC 会话数、输出 Mbps 水位
  • 观众侧首帧时间、卡顿率、重缓冲比(埋点/SDK)

11. 常见坑与修复手记

UDP 被清洗/防火墙误伤

现象:H3 握手失败率高、客户端回退 H2。

处理:分段抓包 + ISP 工单开白;临时把 Alt-Svc 降权,并提升备路权重。

GOP 不对齐导致的黑屏

现象:弱网切档即黑帧。

处理:全链路固定 2s GOP;禁 sc_threshold;打包端检查 CMCD/EXT-X-PROGRAM-DATE-TIME 连贯。

NVENC 会话溢出

现象:GPU 利用率低但掉帧。

处理:调度器加入会话上限;按“路×档数”估算会话,L4 单卡稳态 16–20 路 1080p30×多档。

边缘缓存“被打穿”

现象:m4s 大量回源,packager 飙 CPU。

处理:扩大 max_size,冷热分层;对长尾片段开启 slice;启用 stale-while-revalidate(Nginx 可通过 proxy_cache_use_stale updating 近似)。

跨境链路抖动

现象:观众端 RTT 抖动>80ms。

处理:香港多运营商 BGP,按 ASN 做策略路由;对问题 ASN 下发较高 H2 权重,待运营商侧修复再放开 H3。

12. 变更与回滚 SOP(上线前贴墙)

  •  分阶段开放 Alt-Svc(5%→25%→50%)。
  •  压测覆盖 分辨率×网络×终端 组合,至少 24 组。
  •  入口队列预热,GPU 余量 ≥30%。
  •  回滚一键脚本:撤 Alt-Svc、降 H3 权重、切 H2/CDN 联邦。

 观测阈值告警:

  • H3 握手失败率 > 3%(5 分钟)
  • m4s 回源率 > 20%(3 分钟)
  • 转码队列深度 > 100(2 分钟)

13. 我在线上用的“即插即用”片段

13.1 acme.sh 自动签发(边缘证书)

curl https://get.acme.sh | sh
~/.acme.sh/acme.sh --issue -d edge.hk.example.com --nginx
~/.acme.sh/acme.sh --install-cert -d edge.hk.example.com \
 --key-file       /etc/letsencrypt/live/edge/privkey.pem \
 --fullchain-file /etc/letsencrypt/live/edge/fullchain.pem \
 --reloadcmd     "systemctl reload nginx"

13.2 FRR(BGP 片段)

router bgp 65001
  neighbor 203.0.113.1 remote-as 3491
  neighbor 203.0.113.2 remote-as 9304
  address-family ipv4 unicast
    network 203.0.113.0/24
    maximum-paths 4
  exit-address-family

13.3 bpftrace 快速查 UDP 丢包点

bpftrace -e 'kprobe:udp_recvmsg { @cnt[probe] = count(); } interval:s:5 { print(@cnt); clear(@cnt); }'

14. 结果与复盘:从“红到绿”的峰值 30 分钟

大促当天主场前 10 分钟,我们按 SOP 让 H3 覆盖率从 25% 提到 60%;峰值到来时,H3 RTT 比 H2 低了平均 18–25ms。NVENC 会话水位保持在 0.7–0.8,队列深度最高 62,GPU 扩至 110% 预设副本后回落。
最让我印象深刻的,是凌晨那次 UDP 限流的惊魂:如果没有把 GOP 对齐 和 装箱调度 提前“刻进”系统,我们很可能会在最关键的 5 分钟里失速。

机房的灯又灭了

收尾时,机房的灯按定时器熄掉,只剩交换机和服务器的指示灯在一排排闪。我把保温杯里终于凉透的咖啡一口喝干,给自己留了一条 CMDB 变更备注:“下次大促,直接以 H3 为主,H2 当稳态备胎”。
如果你也要在香港做一套能扛大促的直播系统,我希望这篇笔记能让你少踩几个我踩过的坑。上线前,记得在冷风口走一圈——很多网络问题,只能在风里被发现。

附:检查清单(精简版)

  •  UDP/443 全链路放行(ISP/清洗/防火墙/边缘)
  •  Nginx QUIC 开启 + Alt-Svc 灰度
  •  GOP 全档位对齐 2s,禁场景切
  •  NVENC 会话/显存/带宽三约束装箱调度
  •  HLS/DASH:清单短缓存、分片长缓存、Range + slice
  •  H3/H2 动态权重回退
  •  阈值告警与一键回滚
目录结构
全文