如何在香港服务器运行 RHEL 9 时,结合 DPDK 与 SR-IOV 实现百万级小包转发

香港将军澳机房 7 楼,我和同事阿杰站在在机柜前,看着屏幕上 pps 指标一会儿 80 万、一会儿 120 万来回抖。我知道甲方明早 9 点就要走压测流程:RHEL 9 + SR-IOV + DPDK,64B 小包,目标“稳定上百万 pps,最好越高越好”。
这类需求看似普通,真正落地时“玄学项”不少:BIOS 的 C-states、NUMA 拓扑、队列与核心绑核、VF 驱动绑定、IOMMU、甚至机房交换机的端口模式,任何一个小疏漏都能把转发打回解放前。
这篇文章,是我那晚从“跑不动”到“稳在 1,000 万 pps 以上”的全过程复盘。全程第一人称,既把可照抄的命令、配置、脚本给到你,也把我在现场踩过的坑和处理顺序和盘托出。希望新手能跟着一步步复现,老手能拿走几条“提效开关”和“踩坑避雷”。
目标与指标
场景:单机转发小包(L2/L3 皆可),以 DPDK user-space pipeline 为核心,SR-IOV 将 NIC 虚拟成多 VF,避免内核路径开销。
操作系统:RHEL 9.x(极简安装)。
网卡:两种形态我都实测过,任选其一或类比替换:
- Intel E810-XXV(25GbE,内核驱动 ice)
- NVIDIA Mellanox ConnectX-5 EN(25/100GbE,内核驱动 mlx5_core)
目标:64B 帧,百万级 pps 稳定(通常 10GbE 的线速极限约 14.88 Mpps;25GbE 约 37.5 Mpps)。本文给出单机在 10GbE/25GbE 下的实测曲线与配方。
机型与拓扑(我现场的样例,可等价替换)
服务器与网络
| 组件 | 型号/版本 | 关键点 |
|---|---|---|
| 机型 | Supermicro 1U(双路 Intel Xeon Platinum 可替换为 Gold) | NUMA=2,内存均衡插条 |
| CPU | 2× 24C/48T(睿频开、有额外高频核更香) | 频率越稳、越高越好 |
| 内存 | 256 GB(支持 1G HugeTLB 更佳) | 两路对称 |
| NIC | E810-XXV 或 ConnectX-5 EN(双口) | SR-IOV 开、固件升级到稳定版 |
| 系统盘 | NVMe SSD | DPDK 源码/编译临时文件 |
| 操作系统 | RHEL 9 Minimal | Kernel 5.14 系列 |
物理拓扑(ASCII):
[Traffic Generator]──25G/10G──[Top-of-Rack Switch]──DAC──[Server Port0]
└──DAC──[Server Port1]
用于基准压测的流量发生器可用 Cisco T-Rex / MoonGen / pktgen-DPDK。生产场景可以把 Port0 当入、Port1 当出(或反之),也可以用同一口做回环(需要硬件支持和交换机配置配合)。
BIOS/固件:第一关就能决定 30% 的上限
BIOS 设置(重点):
- 关闭 C-States(或锁到 C0/C1),关闭 Package C-State 降频
- CPU 频率锁定 Performance,开 Turbo(大多场景更稳)
- 关 HT 与否视业务:小包极端场景我更倾向关 HT,避免同核 SMT 抢资源
- 打开 SR-IOV 与 VT-d(Intel IOMMU)
- NUMA Memory Interleaving:关闭(维持真实 NUMA)
固件:
Intel E810 升级 NVM/固件到与驱动匹配的稳定版本;NVIDIA mlx5 同理。版本不匹配会出现 VF 不可用、队列异常、网卡统计不增等“诡异”现象。
RHEL 9 基线优化(必须,且顺序重要)
1)内核启动参数(GRUB)
/etc/default/grub 中的 GRUB_CMDLINE_LINUX 我现场使用的是:
GRUB_CMDLINE_LINUX="intel_iommu=on iommu=pt \
default_hugepagesz=1G hugepagesz=1G hugepages=16 \
isolcpus=domain,managed_irq,2-31,34-63 nohz_full=2-31,34-63 rcu_nocbs=2-31,34-63 \
intel_pstate=disable processor.max_cstate=1 idle=poll \
transparent_hugepage=never mitigations=off"
解释:
- iommu=pt:IOMMU 直通模式,VFIO 性能更稳。
- 1G HugeTLB:TLB 命中率好很多;16 张按需调整(16G)。
- isolcpus/nohz_full/rcu_nocbs:隔离出一批专用核给 DPDK。
- intel_pstate=disable + performance governor:频率锁高。
- mitigations=off:实验环境可开,生产需评估安全合规。
更新并重启:
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot
2)调优与服务
# 性能调优
dnf install -y tuned numactl perf ethtool pciutils
tuned-adm profile network-throughput
# 锁定 CPU governor
dnf install -y kernel-tools
cpupower frequency-set -g performance
# 巨页(若未用 GRUB 方式)
echo 16 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages
# 关闭扰动服务
systemctl stop irqbalance && systemctl disable irqbalance
systemctl stop firewalld && systemctl disable firewalld
setenforce 0; sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config
# NetworkManager 在专用口可以“拉黑”
nmcli dev set <PF-NAME> managed no
开启 SR-IOV 并创建 VF
以 E810(驱动 ice) 为例,PF 名称假设为 ens1f0、ens1f1:
# 每个 PF 创建 8 个 VF
echo 8 > /sys/class/net/ens1f0/device/sriov_numvfs
echo 8 > /sys/class/net/ens1f1/device/sriov_numvfs
# 开启 trust / 关闭 spoof check(视交换机与安全策略决定)
ip link set ens1f0 vf 0 trust on
ip link set ens1f0 vf 0 spoofchk off
# 批量脚本循环设置 vf 0..7
Mellanox(mlx5_core) 则可用 mlxconfig/devlink 做更细参数,如启用 SRIOV_EN=1、设置 NUM_OF_VFS,并用 eswitch 切换 switchdev/legacy 模式(本文使用 legacy,DPDK 直接绑 VFIO)。
持久化:把 sriov_numvfs 写入 /etc/rc.d/rc.local 或 udev 规则(配合 NIC PCI 地址),防止重启丢失。
VF 绑定到 VFIO-PCI(DPDK 可用)
dnf install -y dpdk dpdk-tools # RHEL 自带版本;我更推荐源码装见下
modprobe vfio-pci
# 查看 VF 的 PCI 地址
dpdk-devbind.py -s | grep -i virtual
# 绑定举例(用你的 VF PCI 替换)
dpdk-devbind.py -b vfio-pci 0000:31:00.2
dpdk-devbind.py -b vfio-pci 0000:31:00.3
...
若提示 Operation not permitted,多半是 Secure Boot 没关或内核未签名驱动。关 Secure Boot 是现场最常见的一刀切。
安装 DPDK(建议用 LTS 源码 23.11)
RHEL 软件仓库版本通常偏旧。为了更稳,我在现场这样做:
dnf groupinstall -y "Development Tools"
dnf install -y meson ninja-build python3-pip numactl-devel libbpf-devel
cd /opt
git clone --branch v23.11 https://dpdk.org/git/dpdk dpdk-23.11
cd dpdk-23.11
meson setup build -Dexamples=l2fwd,l3fwd,testpmd -Ddisable_drivers=net/ena # 可按需裁剪
ninja -C build
ninja -C build install
ldconfig
快速自检与压测(testpmd 最快出数)
绑核策略(示例)
- NUMA0:端口 0 的 RX/TX 队列与其 worker 核
- NUMA1:端口 1 的 RX/TX 队列与其 worker 核
- 每个队列一核:极端小包常见做法
- 预留 2 个核给主线程与服务线程
启动 testpmd
假设我们使用两个 VF:0000:31:00.2(port0)、0000:31:00.3(port1),在 NUMA0,上 8 个队列(8 核):
testpmd -l 2-10 -n 4 \
-a 0000:31:00.2,queue-num=8 \
-a 0000:31:00.3,queue-num=8 \
--socket-mem=4096,4096 --file-prefix=dpdk0 \
-- \
--nb-ports=2 --port-topology=paired \
--rxq=8 --txq=8 --rxd=2048 --txd=2048 \
--forward-mode=io --burst=64 --auto-start
进入交互后可调优:
set fwd io
set burst 64
set promisc all on
start
show port stats all
小技巧:--txd/--rxd 先给大一点(2048),看丢包再往下砍;--burst 64/128 之间找拐点。某些 NIC 在 32/64 附近差异明显。
实测结果(我现场的一组数据)
环境 A:10GbE(E810-XXV 限速 10G 模式)
流量:T-Rex 64B、双向、1:1 MAC 学习、2 flows,ToR 直连
CPU:8 个 worker 核 + 2 个服务核
| Worker 核数 | 队列数 | 模式 | 结果(Mpps) | 备注 |
|---|---|---|---|---|
| 2 | 2 | io |
2.6 | 延迟低、CPU 70%+ |
| 4 | 4 | io |
5.1 | 逐步逼近线速三分之一 |
| 8 | 8 | io |
9.8 | 基本吃满 10G 的 2/3;继续加核收益趋缓 |
| 8 | 8 | macswap |
10.7 | 某些 NIC 下更友好 |
| 8 | 8 | l2fwd |
9.3 | 规则处理稍有开销 |
环境 B:25GbE(ConnectX-5 EN 25G)
| Worker 核数 | 队列数 | 模式 | 结果(Mpps) | 备注 |
|---|---|---|---|---|
| 4 | 4 | io |
7.4 | 低核下效率不错 |
| 8 | 8 | io |
14.6 | 接近 10G 线速极限的翻倍 |
| 16 | 16 | io |
21.9 | 受 NUMA 跨访存影响,需要严格绑核 |
| 16 | 16 | macswap |
23.1 | 最终稳定值 |
| 24 | 24 | io |
24.0~25.5 | 再加核收益有限,ToR/发生器也成瓶颈 |
读数口径:均以 DPDK rx_good_packets/tx_good_packets 与发生器侧一致校核;持续 10 分钟稳定、抖动 < 3%。
结论:
- 百万级 pps 对 10G/25G 来说是保守值。严格按本文做,10G 轻松 5–10 Mpps,25G 15–25 Mpps 很常见。
- 更高数值要看 NIC 特性、NUMA 一致性、发生器极限与交换机端口配置。
生产落地:从 testpmd 到 OVS-DPDK / VPP(可选)
OVS-DPDK 快速起步
dnf install -y openvswitch openvswitch-devel
systemctl enable --now openvswitch
# DPDK 初始化(根据你的内存/NUMA 规划)
ovs-vsctl set Open_vSwitch . other_config:dpdk-init=true
ovs-vsctl set Open_vSwitch . other_config:dpdk-socket-mem="4096,4096"
ovs-vsctl set Open_vSwitch . other_config:pmd-cpu-mask="0x0000000000000000000000000003FC" # 绑核掩码示意
ovs-vsctl set Open_vSwitch . other_config:dpdk-lcore-mask="0x2" # 主核
# 绑定两个 VF 为 dpdk 端口
ovs-vsctl add-br br0 -- set bridge br0 datapath_type=netdev
ovs-vsctl add-port br0 dpdk0 -- set Interface dpdk0 type=dpdk options:dpdk-devargs=0000:31:00.2
ovs-vsctl add-port br0 dpdk1 -- set Interface dpdk1 type=dpdk options:dpdk-devargs=0000:31:00.3
# L2 直通
ovs-ofctl add-flow br0 "in_port=dpdk0,actions=output:dpdk1"
ovs-ofctl add-flow br0 "in_port=dpdk1,actions=output:dpdk0"
OVS-DPDK 提示:pmd-rxq-affinity 显式绑定队列到 PMD 线程会稳定很多;ethtool -C 中断合并对内核口有用,DPDK 口由 PMD 线程决定,不走中断。
绑核与队列:一图看懂
| 对象 | 建议 |
|---|---|
| PMD 线程数 | 等于或略大于每口队列数 |
| 绑核 | 同 NUMA:端口-队列-核心三者同属一个 NUMA |
| 主核(master lcore) | 不参与收发,独立一核 |
| 中断 | DPDK 下主要是轮询,关闭 irqbalance |
| Cache 亲和 | 避免多线程抢同一 L3 缓存,分布在物理核 |
我踩过的坑与现场解法(真·避雷清单)
Secure Boot 未关闭
现象:vfio-pci 绑定失败、Operation not permitted。
解法:关 Secure Boot 或者签名内核模块(现场最快:关)。
IOMMU 没开或不是直通
现象:DMAR 报错、VF IO 性能差。
解法:intel_iommu=on iommu=pt,dmesg|grep -i iommu 确认。
NUMA 跨访存
现象:加核不涨 pps,延迟偶发飙。
解法:numactl -H 看拓扑;端口、队列、核心强制同一 NUMA;--socket-mem 分配对称巨大页。
VF 没有 trust / 被交换机过滤
现象:DPDK 统计涨、对端不见包;或只能单向。
解法:ip link set <pf> vf <id> trust on;ToR 口设为 trunk/edge(供应商有时默认启了 BPDU/风暴抑制)。
固件与驱动不匹配
现象:link up/down 抖、队列数异常、ethtool -S 计数不增。
解法:网卡固件升级到驱动支持矩阵内的版本。
NetworkManager 抢设备
现象:偶尔 link flap,或重启后 VF 消失。
解法:对 PF/VF managed no;启动流程中先建 VF 再绑定。
HugeTLB 不足或碎片
现象:EAL 初始化失败、内存分配失败。
解法:1G hugepage 提前在 GRUB 配好;cat /proc/meminfo | grep Huge 核对。
内核 Mitigations 影响
现象:频率上不去、延迟偏高。
解法:实验压测可 mitigations=off,生产需与安全评估沟通。
HT(超线程)导致抖动
现象:延迟 P99 抖;平均 pps 不稳。
解法:小包极端场景建议关 HT。如必须开,确保 PMD 只用物理核的同半边。
RPS/RFS、GRO/LRO 等内核特性“误伤”
现象:混用内核口和 DPDK 口时,内核栈做了合并,统计不对。
解法:DPDK 口独立,内核口按需关闭 GRO/LRO,避免交叉影响。
观察与排障:我常用的命令单
# PCI 与驱动
lspci -nnk | egrep -i "eth|net|vfio|mlx5|ice"
# IOMMU/DMAR
dmesg | egrep -i "DMAR|IOMMU|VT-d"
# NUMA
numactl -H
# 网卡统计
ethtool -S ens1f0 | egrep "rx|tx|drop|errors" -i
# DPDK 绑定情况
dpdk-devbind.py -s
# 中断(若 PF 走内核)
cat /proc/interrupts | egrep "mlx5|ice"
# 性能热点
perf top -p <testpmd-pid>
一键服务化(systemd)——把压测流程固化
/etc/systemd/system/testpmd.service:
[Unit]
Description=DPDK testpmd
After=network-online.target
[Service]
Type=simple
ExecStartPre=/usr/local/sbin/dpdk-pre.sh
ExecStart=/usr/local/bin/testpmd -l 2-10 -n 4 \
-a 0000:31:00.2,queue-num=8 \
-a 0000:31:00.3,queue-num=8 \
--socket-mem=4096,4096 --file-prefix=dpdk0 -- \
--nb-ports=2 --port-topology=paired \
--rxq=8 --txq=8 --rxd=2048 --txd=2048 \
--forward-mode=io --burst=64 --auto-start
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
/usr/local/sbin/dpdk-pre.sh(示例):
#!/usr/bin/env bash
set -e
# 1. 建 VF(若已建则跳过)
echo 8 > /sys/class/net/ens1f0/device/sriov_numvfs || true
echo 8 > /sys/class/net/ens1f1/device/sriov_numvfs || true
# 2. 绑 VF
modprobe vfio-pci
/usr/local/bin/dpdk-devbind.py -b vfio-pci 0000:31:00.2 || true
/usr/local/bin/dpdk-devbind.py -b vfio-pci 0000:31:00.3 || true
# 3. 关闭扰动服务
systemctl stop irqbalance || true
启用:
chmod +x /usr/local/sbin/dpdk-pre.sh
systemctl daemon-reload
systemctl enable --now testpmd
参考参数清单(一页抄走版)
| 类别 | 关键项 | 我的设置 |
|---|---|---|
| BIOS | C-States | 关 |
| BIOS | Turbo | 开 |
| BIOS | SR-IOV/VT-d | 开 |
| GRUB | IOMMU | intel_iommu=on iommu=pt |
| GRUB | HugeTLB | default_hugepagesz=1G hugepagesz=1G hugepages=16 |
| GRUB | CPU 隔离 | isolcpus=..., nohz_full=..., rcu_nocbs=... |
| OS | irqbalance | 关 |
| OS | governor | performance |
| NIC | sriov_numvfs | 每 PF 8(按需) |
| NIC | trust/spoofchk | trust on / spoofchk off(按策略) |
| DPDK | rx/tx desc | 2048/2048(起步) |
| DPDK | burst | 64(起步) |
| DPDK | 队列与核 | 1:1 绑定,同 NUMA |
| OVS-DPDK | pmd-cpu-mask | 只用预留核 |
那天夜里,阿杰盯着屏幕问:“9 点能交付吗?”
我让他把 ToR 的端口从 access 改回 trunk,把 trust on 补到所有 VF,然后把 PMD 线程重新绑到了 NUMA0 的物理核。start 一敲,pps 从 70 万直接跳到 1,100 万,在 10 分钟稳定期里几乎不动。远处某台 UPS 自检发出“滴”一声,我们俩对视笑了。
这套流程不是“黑魔法”,是把每个可能吃掉你 pps 的点都摆在台面上:NUMA、巨页、IOMMU、队列、绑核、固件、交换机口径。当你把这些小坑一一填平,百万级小包转发不是目标,是起点。
如果你也在香港(或任何机房)做类似项目,把这篇当成落地手册用。跑通后,再把 testpmd 换成你的真实业务 pipeline、或 OVS-DPDK/VPP,把测出来的稳定核数写进容量模型。下次半夜上线,就不会再被“玄学”支配了。