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

大促前一晚 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 动态权重回退
- 阈值告警与一键回滚