如何在香港服务器的 CentOS 7 上部署直播平台,用 SR-IOV 网卡直通把 RTMP 延迟打下来

香港机房凌晨 2 点半,OBS 端的弹幕里有人抱怨“卡”“慢”,而我盯着 Grafana 上那条抖动的 RTT 曲线,心里很清楚:虚拟化层的网络栈成了瓶颈。要么今晚解决,要么明早继续挨喷。于是我决定把生产宿主机的万兆网卡改成 SR-IOV 直通,把直播的 RTMP 流从内核桥里“解放”出来,绕开多余的队列和拷贝。
这篇文章,是我那一晚从 BIOS 到驱动、从中断亲和到 Nginx-RTMP 配置的完整实操复盘。尽量写到“有温度”的细节,也把“坑”摊开讲。无论你是第一次在香港机房上手,还是已经有多年经验,我希望你能照着做,把延迟稳稳降下来。
1. 现场背景与目标
机房/地域:香港柴湾(典型跨境流量:内地、东南亚居多)
宿主机:
- CPU:Intel Xeon Silver 4210(10C/20T)×1
- 内存:64 GB
- 系统盘:NVMe 1 TB
- 网卡:Intel X710-DA2(10 GbE ×2,驱动 i40e,支持 SR-IOV)
系统:CentOS 7.9(3.10 内核,KVM/Libvirt)
虚拟机:CentOS 7.9 作为直播接入节点(Nginx-RTMP,不转码,只做接入与分发/回源)
问题:OBS 推流 RTMP(TCP)在晚高峰端到端首帧延迟 1.2–1.5 s,偶发 2 s+ 抖动
目标:不改业务架构的前提下,优先优化网络路径与内核栈,将 首帧延迟 拉到 < 800 ms、P95 抖动压到 1 s 内
2. 架构与思路:为什么是 SR-IOV 直通
KVM 的常规网络(Linux Bridge / Open vSwitch + veth/tap)在高并发小包流 + TCP 下,容易在宿主机的软中断、qdisc、桥转发、拷贝上叠加延迟。SR-IOV 让物理功能(PF)切出多个虚拟功能(VF),把 VF 直通给虚机,虚机里的网卡驱动直接跟硬件队列打交道:
- 少一次虚拟交换(bridge/OVS)
- 少几次 skb 分配/拷贝
- 中断更可控(队列→vCPU 亲和)
简单说:更少的路径、更少的排队、更可预测的中断。
3. 前置检查:硬件/固件/BIOS
BIOS 打开虚拟化 & IOMMU
- Intel VT-d:Enabled
- SR-IOV:Enabled(不同厂牌有时藏在 “PCIe/IO Features”)
网卡固件
ethtool -i ens2f0
# driver: i40e
# version: 2.18.9
# firmware-version: 8.40 0x8000xxxx
现场我遇到过旧固件导致 VF 无法设 MTU 的坑,升级固件后恢复正常。
PCIe 拓扑/IOMMU 组
lspci -nn | grep -E "Ethernet|Network"
lspci -t
4. 宿主机系统准备(CentOS 7)
4.1 升级并选择性能调优
yum makecache fast
yum -y update
yum -y install epel-release tuned numactl pciutils ethtool irqbalance htop
# 低延迟场景用 tuned 的 latency-performance
tuned-adm profile latency-performance
可选(内核 BBR):如果链路跨境抖动明显,我会装 ELRepo 的新内核启用 BBR(TCP 拥塞控制对弱网端有帮助)。这是可选项,今晚我们先不换内核,把变量控制住。
4.2 开 IOMMU + VFIO
编辑 /etc/default/grub(或 /etc/sysconfig/grub):
GRUB_CMDLINE_LINUX="crashkernel=auto rhgb quiet intel_iommu=on iommu=pt"
写入并重建:
grub2-mkconfig -o /boot/grub2/grub.cfg
echo -e "vfio\nvfio-pci\nvfio-iommu-type1" > /etc/modules-load.d/vfio.conf
reboot
重启后确认:
dmesg | grep -e IOMMU -e DMAR
5. 创建 SR-IOV VF 并持久化
以 ens2f0 为例(X710 PF 口 0):
# 查看最多能切几个 VF
cat /sys/class/net/ens2f0/device/sriov_totalvfs
# 生成 4 个 VF
echo 4 > /sys/class/net/ens2f0/device/sriov_numvfs
# 列出 VF
ip link show ens2f0
# 典型输出:vf 0/1/2/3 ...
给每个 VF 设定基础属性(示例:vf 0 作为直播接入口,打 VLAN 200):
# 设 VLAN(若上游是 Trunk)
ip link set dev ens2f0 vf 0 vlan 200
# 为需要容器/多 MAC 的场景可以启用 trust
ip link set dev ens2f0 vf 0 trust on
# 关闭反欺骗(某些上游或者抓包场景需要)
ip link set dev ens2f0 vf 0 spoofchk off
# 设 MTU(注意上游交换机)
ip link set dev ens2f0 vf 0 mtu 1500
坑 1:有的运营商/托管商在汇聚侧不允许 VLAN Tag,Trunk 要提前申请。
坑 2:重启后 VF 会“丢”,要持久化。我用 systemd 单元做:
/usr/local/sbin/sriov-setup.sh:
#!/bin/bash
set -e
PF=ens2f0
VFS=4
if [ -e /sys/class/net/$PF/device/sriov_numvfs ]; then
echo 0 > /sys/class/net/$PF/device/sriov_numvfs || true
echo $VFS > /sys/class/net/$PF/device/sriov_numvfs
ip link set dev $PF vf 0 vlan 200 spoofchk off trust on
ip link set dev $PF vf 1 vlan 201 spoofchk off trust on
ip link set dev $PF vf 2 vlan 202 spoofchk off trust on
ip link set dev $PF vf 3 vlan 203 spoofchk off trust on
fi
/etc/systemd/system/sriov-setup.service:
[Unit]
Description=SR-IOV VFs Setup
After=network-pre.target
Wants=network-pre.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/sriov-setup.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
chmod +x /usr/local/sbin/sriov-setup.sh
systemctl enable --now sriov-setup.service
6. 找到 VF 的 PCI 号并直通给虚机
6.1 确认 VF 的 PCI 地址
# PF 的设备目录里能看到 VF 对应的 PCI
ls -l /sys/class/net/ens2f0/device/ | grep virtfn
# 形如 virtfn0 -> ../../../0000:18:00.2
把 0000:18:00.2 这样的地址记下来。
6.2 使用 VFIO 直通(libvirt XML)
示例虚机(省略无关项):
<devices>
<!-- HugePages(可选) -->
<memoryBacking>
<hugepages/>
</memoryBacking>
<!-- 直通 VF:0000:18:00.2 -->
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x18' slot='0x00' function='0x2'/>
</source>
<driver name='vfio'/>
</hostdev>
</devices>
坑 3:有些平台 IOMMU 组会把 PF/VF 与其他设备绑一起,导致无法单独直通。若确实被 ACS 限制,需要在主板 BIOS 或更换槽位规避。不建议使用内核 ACS override(风险大)。
6.3 vCPU/中断亲和与 NUMA
把 VF 对应的中断绑定到固定核,避免抖动:
# 找 VF 的 IRQ
grep -i vf /proc/interrupts
# 把中断绑到 CPU 2(示例)
echo 4 > /proc/irq/$(IRQ_ID)/smp_affinity
vCPU 绑核(libvirt/virt-manager 里 pin 到同一 NUMA 节点),尽量做到:VF 中断核 与 虚机 vCPU 在同一个 NUMA。
7. 虚机内网络优化(拿到 VF 后)
登陆虚机(CentOS 7),确认驱动:
ethtool -i eth0
# Intel X710 VF 大概率还是 i40e/ixgbevf 系列
7.1 关闭会增加延迟的合并/分段(按需)
低延迟优先时,我会关掉 VF 上的 GRO/LRO,保守起见也常关 TSO/GSO,牺牲一些 CPU 换稳定的毫秒级延迟:
ethtool -K eth0 gro off lro off gso off tso off tx-nocache-copy on
# 队列/环形缓冲适当缩小,减少排队(VF 上可能不可调)
ethtool -G eth0 rx 512 tx 512
# 中断合并(coalesce)趋向于小/零
ethtool -C eth0 rx-usecs 0 tx-usecs 0
坑 4:有些云/托管平台禁用 VF 上的某些 ethtool 参数;遇到 Operation not supported 就别死磕,改到 PF 侧或接受默认。
7.2 TCP 栈与队列
/etc/sysctl.d/99-live.conf:
# 基础网络队列
net.core.netdev_max_backlog = 5000
net.core.somaxconn = 1024
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
# TCP 窗口/缓冲
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_sack = 1
# 低延迟倾向
net.ipv4.tcp_low_latency = 1
sysctl --system
若你已升级到新内核,建议启用 BBR:net.ipv4.tcp_congestion_control=bbr。
7.3 文件句柄与进程限制
echo "* soft nofile 1048576" >> /etc/security/limits.conf
echo "* hard nofile 1048576" >> /etc/security/limits.conf
ulimit -n 1048576
8. Nginx-RTMP(只做接入/回源)的低延迟实践
我这里选 Nginx-RTMP 做 RTMP Ingest(不做 HLS/DASH,完全绕过以避免额外秒级延迟),核心思路是:不转码、不缓存、尽快转发。
安装(简):
yum -y install nginx
# 编译 nginx-rtmp-module(省略细节,或使用你现有打包)
# 也可以用容器化,但 SR-IOV 已经给了我们最直接的网络路径
配置 /etc/nginx/nginx.conf 片段(关键项注释在行尾):
rtmp {
server {
listen 1935;
# RTMP chunk 越小越低延迟(但 CPU 多),4096 是折中
chunk_size 4096;
application live {
live on; # 直播模式
interleave on; # 音视频交织更及时
meta copy; # 透传元数据
wait_key off; # 不强制等关键帧,首帧更快(播放器需容错)
drop_idle_publisher 10s;
# 回源/转发(示例:推到上游/CDN)——按需开启
# push rtmp://upstream/live;
}
}
}
events {
worker_connections 20480;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on; # 对 RTMP 的 TCP 也有帮助(内核层)
keepalive_timeout 65;
types_hash_max_size 4096;
server_tokens off;
}
要点:
- 不做 HLS(HLS 天然秒级延迟);RTMP→RTMP 直转发。
- wait_key off 会让首帧更快,但有些播放器可能在第一帧前黑一下;稳妥方案是 把推流端 GOP 设小(见下)。
推流端(OBS/FFmpeg)建议:
ffmpeg -re -stream_loop -1 -i demo.mp4 \
-c:v libx264 -preset veryfast -tune zerolatency -x264-params "scenecut=0:open_gop=0:keyint=30:min-keyint=30" \
-c:a aac -b:a 128k -ar 44100 \
-f flv -rtmp_buffer 0 rtmp://your_ingest/live/stream001
30 fps 时 keyint=30 → 1 s GOP;更激进可 0.5 s(keyint=15)。
-tune zerolatency、-rtmp_buffer 0 能减少推流端的缓冲。
9. 实测与对比
我用下面的方法做 端到端首帧 的“土办法”测量:
- 推流端视频叠加实时时间戳(drawtext=text='%{localtime}')
- 拉流端抓到第一帧画面时间戳,与拉流机本地时钟对齐(NTP 同步)
- 每轮 5 分钟,统计 P50/P95
表 1:切 SR-IOV 前后(晚高峰 21:00–22:00)
| 指标 | 变更前(Linux Bridge) | 变更后(VF 直通) |
|---|---|---|
| 首帧 P50 | 1.28 s | 0.69 s |
| 首帧 P95 | 2.02 s | 0.96 s |
| 传输 RTT 抖动(P95) | 35 ms | 18 ms |
| CPU(宿主软中断%) | 35–45% | 15–20% |
| 丢包(ifconfig RX errors) | 偶发攀升 | 基本消失 |
解释:首帧下降主要来自少了一层虚拟交换、中断路径更短、GRO/LRO 关闭减少队列滞留。CPU 下降是“钱花在刀刃上”:软中断少了,但 VF 上的 per-packet 成本可控,整体更稳。
10. 进一步的“压榨”:中断/队列/亲和脚本
我现场保留了一个“粗暴有效”的亲和脚本,把 VF 的 IRQ 绑到同一物理核,同时把 Nginx worker 绑到邻核,减少跨核迁移:
#!/bin/bash
# /usr/local/sbin/rtmp-affinity.sh
set -e
VF_PCI="0000:18:00.2"
# 找 IRQ 列表
IRQS=$(grep $(basename $(readlink /sys/bus/pci/devices/$VF_PCI/msi_irqs/*)) -R /proc/interrupts | awk -F: '{print $1}')
# 把所有 IRQ 绑到 CPU2
for i in $IRQS; do
echo 4 > /proc/irq/$i/smp_affinity
done
# 绑 Nginx worker 到 CPU3
pgrep -f "nginx: worker process" | xargs -I{} taskset -pc 8 {}
提示:smp_affinity 的值是位掩码,4 表示 CPU2(从 0 开始)。请按你的 CPU 拓扑调整。
11. 常见坑位与解法
坑 A:VF 起不来 / “Resource temporarily unavailable”
先把 sriov_numvfs 写 0,再写目标值;确认没有 VM 占用旧 VF。
坑 B:MTU 不生效
上游交换机 / 运营商边口没放开;或 VF 驱动不支持 JUMBO。直播 RTMP 用 1500 就好,稳。
坑 C:VLAN Tag 被“吃”了
有的托管商只给 Access 口;用 tcpdump -i pf 抓一眼是否带 vlan。
坑 D:VF 报错 Operation not supported
很多 ethtool 选项在 VF 上被限制(安全、隔离考虑),不强求;把队列与 coalesce 尽量靠近“低延迟”。
坑 E:wait_key off 播放器开头黑屏
两端都要配合:把 GOP 缩小 才是真“低延迟”的王道;或者接受 0.5–1 s 的稳健首帧。
12. 可选增强
切到 OVS-DPDK? 如果你的平台允许、并且业务是多租户复杂二层,OVS-DPDK 也能做出接近 SR-IOV 的路径,但复杂度上去不少。单租户直播 Ingest,我优先 SR-IOV。
多端口分流:PF0/VF0 专职 Ingest、PF1/VF1 专职回源/CDN,避免共享队列导致抖动。
链路侧 QoS:在运营商边界做 TCP 优先级/RED,可以再抹掉一点 Tail 延迟(需配合网络团队)。
13. 复盘总结与 checklist
落地 Checklist(可以直接照抄给同事)
- BIOS:VT-d / SR-IOV Enabled
- 内核:intel_iommu=on iommu=pt 生效
- VF 创建与持久化:sriov_numvfs + systemd
- VF 属性:VLAN / trust / spoofchk / MTU
- VFIO 直通:确认 IOMMU 组、libvirt XML 配好
- NUMA/亲和:IRQ → 固定核;vCPU → 同一 NUMA
- 虚机内 ethtool:GRO/LRO/TSO/GSO 按需关闭、coalesce 降低
- sysctl:TCP 缓冲、低延迟参数
- Nginx-RTMP:不转码、不落 HLS、合理 chunk、wait_key 策略
- 推流端:-tune zerolatency + 小 GOP(≤1 s)
- 实测:首帧 P50/P95、软中断占比、RX/TX error、MTR
14. 我用到的关键命令/文件一览(速查)
# 确认 IOMMU
dmesg | egrep "DMAR|IOMMU"
# 创建/清理 VF
echo 0 > /sys/class/net/ens2f0/device/sriov_numvfs
echo 4 > /sys/class/net/ens2f0/device/sriov_numvfs
ip link set dev ens2f0 vf 0 vlan 200 trust on spoofchk off
# 找 VF 的 PCI
ls -l /sys/class/net/ens2f0/device/ | grep virtfn
# VF 中断与亲和
grep -i vf /proc/interrupts
echo 4 > /proc/irq/$IRQ/smp_affinity
# ethtool 调优(虚机里)
ethtool -K eth0 gro off lro off gso off tso off
ethtool -C eth0 rx-usecs 0 tx-usecs 0
15. 尾声:凌晨 4 点,曲线“平”了
我把 OBS 再次推上去,眼角余光瞄着手机播端。那条首帧折线安静地贴在 700–800 ms 之间,偶尔抖一下也止步 900+ ms。Grafana 的软中断面板第一次这么顺眼;弹幕里“不卡了”的反馈开始出现。我收起螺丝刀,合上机柜门,冷气依旧,但那会儿感觉已经不刺骨了。
这就是我在香港服务器上,把 RTMP 低延迟做“粗中有细”的一次落地。SR-IOV 直通不是银弹,但对“单租户、单功能、低延迟优先”的接入节点,它常常是最划算的那一刀。