跨境直播上行优化:香港服务器支撑的回国链路抖动治理、FEC与自适应码率权衡

深夜两点,湾仔机房的冷风直往机柜缝里钻。双十一预热场刚开,主播那边的聊天窗口开始刷“卡”“糊”“延迟高”。我盯着 Prometheus 的抖动曲线——回国链路的抖动飙到了 35ms、瞬时丢包 2.4%。香港到广州的常规 18ms RTT 化成了“锯齿”。那一刻,我知道靠“玄学重推流”救不回来,必须动刀到传输链路、队列与编码参数。下面是那一夜我在香港两台上行汇聚服务器上,如何把问题一刀刀剖开的全过程。
1)业务场景与目标
场景:主播端(东南亚/华南)→ 香港上行汇聚(转码/抖动治理)→ 回国链路(两路回程)→ 内地播控/CDN 接入 → 观众。
目标:
把上行端到端时延稳定在 800–1200ms 内(含转码与回传)。
链路抖动 95 分位 < 5ms,有效去抖;丢包 95 分位 < 0.5%。
在 1–3% 间歇性丢包下,画面可感卡顿次数 < 2 次/小时。
自适应码率(ABR)与冗余(FEC/ARQ)之间做到合理权衡:保证画质“渐退不崩”。
2)拓扑与回国链路设计
主播端(OBS/FFmpeg SRT)
│ UDP/SRT(ARQ) 主推
│
▼
[香港汇聚边缘A]───VRRP/VIP───[香港汇聚边缘B] ←→ 共享转码池(GPU/CPU)
│ │
├───────────回国专线/隧道#1(CN2/GIA/GRE)──────────► 内地播控/接入1
└───────────回国隧道#2(CMI/CTG/GRE)─────────────► 内地播控/接入2
上行协议:主播→香港用 SRT(UDP/ARQ),低时延且对抖动友好。
回国:香港→内地走 双隧道(GRE over 专线或跨网),上面跑 SRT/QUIC(按对端接入支持而定);策略路由做动态降级与切换。
汇聚:两台香港边缘机以 VRRP(keepalived) 提供 VIP,任何一台维护/故障不影响入口。
3)硬件与系统参数(香港汇聚/转码)
3.1 设备与系统(CentOS 7.9,稳定优先)
| 角色 | 机型/CPU | 内存 | 系统盘 | 数据盘 | 网卡 | OS/内核 | 备注 |
|---|---|---|---|---|---|---|---|
| 汇聚边缘A/B | AMD EPYC 7313P(16C/32T) | 128GB ECC | SATA SSD 480G | NVMe U.2 3.84TB(如 PM9A3) | Mellanox ConnectX-4 Lx 25GbE ×2 | CentOS 7.9 + ELRepo kernel 5.4.x | NUMA 单路、IPMI 远控 |
| 转码节点×2 | Intel Xeon Silver 4310 | 192GB ECC | SATA SSD 480G | NVMe 1.92TB | 10GbE Intel X710 | CentOS 7.9 + CUDA 12.x(可选) | NVENC/LVVA |
为何这么配:
汇聚边缘更看重IO 与网络;NVMe 用于环形缓存/时移缓冲,Mellanox 网卡支持更稳的硬件时间戳与队列调度。
CentOS 7.9 搭配 ELRepo 5.4 LTS 内核是为拿到 BBR/fq_codel/ethtool offload等新特性,同时保持运维栈的稳定性。
3.2 网卡与内核调优
/etc/sysctl.d/99-live.conf:
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.core.netdev_max_backlog = 250000
net.ipv4.udp_rmem_min = 262144
net.ipv4.udp_wmem_min = 262144
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.ip_local_port_range = 10000 65000
net.core.default_qdisc = fq_codel
网卡与中断亲和(确保多队列不互踩):
# 关闭可能影响时延的合并(按链路实际测试微调)
ethtool -K eth0 gro off lro off gso on tso on
# RPS/XPS 按队列绑核
echo ffff > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo ffff > /sys/class/net/eth0/queues/tx-0/xps_cpus
# 提升环形缓冲
ethtool -G eth0 rx 4096 tx 4096
队列治理(入口/出口 fq_codel):
tc qdisc replace dev eth0 root fq_codel limit 800 target 5ms interval 100ms ecn
tc qdisc replace dev eth0 handle 1: root fq_codel target 5ms ce_threshold 1ms
4)上行协议与关键参数(SRT 为主,RTMP 兜底)
4.1 主推:SRT(主播→香港)
参数建议(OBS/FFmpeg 都适用):
| 参数 | 建议值 | 说明 |
|---|---|---|
latency |
80–120ms | 抖动缓冲,过低会“抖壳”,过高拖时延 |
rcvbuf/sndbuf |
4–8MB | 与 rmem_max/wmem_max 对齐 |
tlpktdrop |
1 |
拥塞时丢旧包,保持实时性 |
lossmaxttl |
20–40 | 最大重传 TTL,抖动剧烈场景放宽 |
pbkeylen |
16/24/32 | AES 加密长度,按合规要求 |
FFmpeg 推流示例(主播侧或边缘编码器):
ffmpeg -re -i input.mp4 -c:v libx264 -preset veryfast -tune zerolatency \
-g 60 -keyint_min 60 -sc_threshold 0 -bf 0 -b:v 6000k -maxrate 6000k -bufsize 3000k \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-f mpegts "srt://hk-edge.example.com:10080?mode=caller&transtype=live&latency=100&sndbuf=8388608&rcvbuf=8388608&tlpktdrop=1"
4.2 兜底:RTMP(TCP)
跨境偶有 UDP 端口择优问题,某些移动/校园网对 UDP 不友好。
兜底走 RTMP(TCP)仅作保活,大抖动场景会出现长尾卡段,自动降级时触发。
5)香港转码与 ABR 梯度
思路:香港只收一条 mezzanine(高码率 1080p),在边缘转出多档 ABR;回国链路按健康度选择合适档位转发,优先稳帧不黑屏。
| 档位 | 分辨率 | 视频码率 | 音频 | GOP/帧率 |
|---|---|---|---|---|
| 1080p | 1920×1080 | 6.0 Mbps | AAC 128k | GOP=2s / 30fps |
| 720p | 1280×720 | 3.0 Mbps | AAC 128k | GOP=2s / 30fps |
| 540p | 960×540 | 1.8 Mbps | AAC 96k | GOP=2s / 30fps |
| 480p | 854×480 | 1.2 Mbps | AAC 96k | GOP=2s / 30fps |
| 360p | 640×360 | 0.8 Mbps | AAC 64k | GOP=2s / 30fps |
转码(香港):
ffmpeg -re -i srt://:10080?mode=listener&transtype=live&latency=100 \
-filter_complex "\
[0:v]split=4[v1080][v720][v540][v360]; \
[v720]scale=-2:720[v720o]; \
[v540]scale=-2:540[v540o]; \
[v360]scale=-2:360[v360o]" \
-map [v1080] -c:v:0 libx264 -b:v:0 6000k -maxrate:v:0 6000k -bufsize:v:0 3000k -preset veryfast -tune zerolatency -x264opts "no-scenecut" \
-map [v720o] -c:v:1 libx264 -b:v:1 3000k -maxrate:v:1 3000k -bufsize:v:1 1500k -preset veryfast -tune zerolatency \
-map [v540o] -c:v:2 libx264 -b:v:2 1800k -maxrate:v:2 1800k -bufsize:v:2 900k -preset veryfast -tune zerolatency \
-map [v360o] -c:v:3 libx264 -b:v:3 800k -maxrate:v:3 800k -bufsize:v:3 400k -preset veryfast -tune zerolatency \
-map a:0 -c:a aac -b:a 128k -ac 2 -ar 48000 \
-f tee "[select=v:0,a:0:f=mpegts]srt://cn2-pop:11080?mode=caller&latency=120|\
[select=v:1,a:0:f=mpegts]srt://cn2-pop:12080?mode=caller&latency=120|\
[select=v:2,a:0:f=mpegts]srt://cn2-pop:13080?mode=caller&latency=120|\
[select=v:3,a:0:f=mpegts]srt://cn2-pop:14080?mode=caller&latency=120"
做法:四档同时“热备”往同一内地接入的不同端口发。接入侧或香港的“指挥器”按链路健康切换 哪一档接入到主播的上游 pipeline(见第 7 节)。
6)抖动治理:队列、缓冲与 ARQ/FEC 权衡
6.1 抖动来源与治理手段
微突发(micro-burst):越洋/跨网边界出现,表现为瞬间队列挤压。
治法:汇聚口 fq_codel + 适度 pacing;SRT latency=100±20ms 作为抖动缓冲。
低速率丢包(1–3%):多数由回国出口(或省际段)发散。
治法:SRT ARQ 先顶一层,必要时在 回国隧道上做轻度 FEC(10–15% 冗余),见下表。
UDP 被限/路由不稳:移动/教育网偶发。
治法:兜底 RTMP/TCP 路径 + 自动降档;或切二回程。
6.2 FEC vs ABR 的数学权衡
设原始码率 R,FEC 冗余比 p,有效码率 R' = R * (1 + p)。
当链路容量 C 固定时,R' < C 才不拥塞。
建议:
当丢包 ≤ 1%:不开 FEC,纯 ARQ(SRT)+ latency 100ms 即可。
1–3%:FEC 10–12%(XOR/Pro-MPEG 级别)或放宽 ARQ TTL,并降一档 ABR。
>3% 且持续 10s+:FEC 15–20% + 至少降两档 ABR。
| 丢包(1s 均值) | 抖动 P95 | 建议 FEC 冗余 | ABR 动作 | 备注 |
|---|---|---|---|---|
| ≤0.5% | ≤5ms | 0 | 保持 | 纯 ARQ |
| 0.5–1% | 5–10ms | 0–8% | 保持/酌情降一级 | 观察 10s |
| 1–3% | 10–20ms | 10–12% | 降一级 | 稳帧优先 |
| >3% | >20ms | 15–20% | 降两级 | 触发兜底路径 |
落地说明:我在回国 GRE 隧道上做轻度 FEC(XOR 块级),保证 1–3% 的随机丢失不炸 GOP;FEC 只在回国段开,主播→香港段维持纯 ARQ 低时延。
7)“链路指挥器”:基于健康度的 ABR 选择与回程切换
7.1 监测指标
mtr/smokeping 到内地接入的 RTT、Jitter、Loss(每 2s 采样)。
SRT 统计(srt-live-transmit --stats 或接入侧 API):重传率、队列时延。
阈值(经验值,按你网路实测微调):
Loss_10s_avg > 1.5% 或 Jitter_P95 > 15ms → 降一级;
持续 30s 无异常 → 尝试升一级;
Loss_5s_avg > 3% → 直接降两级并切回程。
7.2 简化实现(示例脚本思想)
#!/usr/bin/env bash
# check_link.sh: 采集链路健康度 → 写入 etcd/redis → 由切换器订阅
MTR_TARGET="cn2-pop.example.com"
LOSS=$(mtr -rwzc 20 $MTR_TARGET | awk '/^SUM/ {print $3+0}')
JITTER=$(mtr -rwzc 20 $MTR_TARGET | awk '/^SUM/ {print $8+0}') # 最后一列按版本可能是 Javg/Jmax,需对齐
# 写入 KV;省略鉴权
curl -s -XPUT http://127.0.0.1:2379/v2/keys/link/health -d value="{\"loss\":$LOSS,\"jitter\":$JITTER}"
切换器订阅健康度,在香港边缘控制“哪一档端口”暴露给内地接入(四档已在第 5 节持续推送):
# 伪代码:当触发降级时,将 11080 -> 720p 对应的 12080
iptables -t nat -R PREROUTING 1 -p udp --dport 11080 -j REDIRECT --to-ports 12080
# 或者用 haproxy/keepalived + SRT relay,改后端端口并 reload
这样内地接入侧无需重推拉,“主口”一直是 11080,但被重定向到不同档位端口。
8)回国隧道与策略路由
8.1 建隧道(GRE 示意)
ip tunnel add gre-cn2 mode gre remote <CN2_POP_IP> local <HK_EDGE_IP> ttl 255
ip link set gre-cn2 up
ip addr add 10.10.10.1/30 dev gre-cn2
ip route add <INGEST_1_IP>/32 dev gre-cn2
ip tunnel add gre-cmi mode gre remote <CMI_POP_IP> local <HK_EDGE_IP> ttl 255
ip link set gre-cmi up
ip addr add 10.10.10.5/30 dev gre-cmi
ip route add <INGEST_2_IP>/32 dev gre-cmi
8.2 标记与策略路由(回程切换)
# 将不同目的端口(11080/12080/...)标记不同路由表
iptables -t mangle -A OUTPUT -p udp --dport 11{0,2,3,4}80 -j MARK --set-mark 11
iptables -t mangle -A OUTPUT -p udp --dport 12{0,2,3,4}80 -j MARK --set-mark 12
ip rule add fwmark 11 table 11
ip rule add fwmark 12 table 12
ip route add default dev gre-cn2 table 11
ip route add default dev gre-cmi table 12
注意 MTU:GRE 有开销,建议设置 MPEG-TS 包(1316B)+ UDP/IP 头后不碎片。必要时 -pkt_size 1316 并将隧道 MTU 降至 1476/1436(视底层而定)。
9)监控与 SLO:我们如何证明“稳定了”
关键面板:
链路面:RTT/Jitter/Loss(smokeping/mtr-exporter)、SRT 重传率/队列延迟。
业务面:每分钟缓冲事件数、直播端到端延迟、码率档位切换次数。
| 指标 | 优化前(均值/P95) | 优化后(均值/P95) |
|---|---|---|
| 回国 RTT | 24ms / 38ms | 20ms / 27ms |
| 抖动 Jitter | 11ms / 35ms | 3.2ms / 6.8ms |
| 丢包(1s 均值) | 1.6% / 3.1% | 0.3% / 0.9% |
| 端到端时延 | 1600ms / 2400ms | 980ms / 1300ms |
| 观众卡顿/小时 | 6.2 次 | 1.1 次 |
这些数字来自那晚至次日午前 6 小时区间;期间有 3 次 ABR 自动降一档,1 次切换回程。
10)常见坑与现场解法
UDP 被限或跨运营商抖动:移动/教育网高峰时段对 UDP 不友好。
解法:自动切 RTMP/TCP 兜底 + 降档;并将主播端引导到更友好的出口(换 Wi-Fi/拉 4G)。
GRO/LRO 关闭过头:全关会让 CPU 飙高、丢包上升。
解法:只在上行口按需关;后台转码口保留 GSO/TSO。
MTU 碎片:GRE + SRT + MPEG-TS 组合若 MTU 设置不当,轻微抖动就“雪崩”。
解法:包长 1316B,隧道 MTU 明确到位,观测 DF/Frag。
FEC 开太大:10%→20% 冗余直接把有效码率抬高,反而挤满链路。
解法:只在回国段开轻度 FEC,且联动 ABR 下切。
单机热升级:换内核/驱动时忘了 VRRP 切从。
解法:双机 VRRP + 升级前“空口切主”,自动化检查流量归零再动手。
11)风控与演练清单(我们真这么做)
11.1 风险矩阵
| 风险 | 触发 | 影响 | 预案 |
|---|---|---|---|
| 回程#1 抖动/丢包暴涨 | 高峰或路由漂移 | 观众卡顿 | 自动切回程#2 + ABR 降级 |
| 香港边缘单机故障 | 硬件/升级 | 中断入口 | VRRP 切换 + 流量归零校验 |
| UDP 被限/阻断 | 运营商策略 | 断流 | 启用 RTMP 兜底 + 时延容忍 |
| 过度拥塞 | FEC/码率叠加 | 黑屏/爆音 | FEC 降级 + 码率下切两档 |
11.2 演练脚本(每周例行)
切回程演练:人为提升 gre-cn2 的丢包到 3%(tc qdisc add ... netem loss 3%),确认 30s 内切至 gre-cmi,并在 5 分钟后恢复。
ABR 下切演练:注入 loss 1.8% + jitter 18ms,验证 11080 → 12080 映射更替。
单机维护演练:VRRP 手工漂移,核对业务无感,确认流量归零再升级内核。
UDP 兜底演练:iptables 限制 UDP 10080,确认自动切到 RTMP 并回切成功。
12)成本与资源测算(给老板看的速算表)
| 项目 | 数量 | 单价/月(示意) | 小计 |
|---|---|---|---|
| 香港 1U 边缘(25G口、双上联) | 2 | x | 2x |
| 转码节点(含 NVENC) | 2 | y | 2y |
| 回国双回程(100–200Mbps 保底) | 2 | z | 2z |
| 监控/带外 | 1 | 0.1z | 0.1z |
| 合计 | ≈ 4x + 2z + 0.1z |
经验:把钱尽量花在回程质量和监控上,硬件多半能“抠”出来。
13)完整配置碎片(可直接抄)
keepalived(VRRP,香港两机)
/etc/keepalived/keepalived.conf(A 为 MASTER,B 为 BACKUP):
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 110
advert_int 1
authentication {
auth_type PASS
auth_pass <REDACTED>
}
virtual_ipaddress {
192.0.2.10/24 dev eth0
}
track_script {
chk_srt
}
}
vrrp_script chk_srt {
script "/usr/local/bin/check_srt.sh"
interval 2
fall 2
rise 3
}
/usr/local/bin/check_srt.sh(端口健康):
#!/usr/bin/env bash
nc -uz 127.0.0.1 10080 2>/dev/null
exit $?
iptables DSCP(隧道内 QoS)
iptables -t mangle -A PREROUTING -p udp --dport 11080 -j DSCP --set-dscp 46 # EF
iptables -t mangle -A PREROUTING -p udp --dport 12080 -j DSCP --set-dscp 34 # AF41
注:公网跨网不一定保 DSCP,但在自建隧道两端可以识别和调度。
凌晨三点半,观众弹幕里“卡”的频次明显下去了,ABR 在 30 分钟内只降了两次,画面稳定、声音不再“爆”。我把 mtr 的抖动曲线和 Alertmanager 的降级记录贴在群里,主播只回了四个字:“稳,就这套。”
机房的风还是冷,但我把外套拉链拉上,盯着 VRRP 的心跳包一闪一灭,像是在看一口“稳态”的心脏。跨境直播的本质不是追求零丢包、零抖动,而是在“不可控”上,搭出一套可预期的有控系统——发生、检测、限流、降级、兜底、回切。
第二天上午回看报表,我给自己留了三条 TODO:
1)把 FEC 模块从 10% 做成自适应(随时延与重传率调宽窄);
2)把指挥器改成 etcd + Lua 的 HAProxy 动态后端,不再靠 iptables;
3)把转码池再拆一台做高刷专用,避免高峰复用抖动。
就这样,这一夜留在了机柜前,也变成了后来每次大促之前的标准动作与“手感”。