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

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

发布人:Minchunlin 发布时间:2025-09-06 10:23 阅读量:831


香港机房凌晨 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 直通不是银弹,但对“单租户、单功能、低延迟优先”的接入节点,它常常是最划算的那一刀。

目录结构
全文