☰
CentOS 安装 DPDK:大页、IOMMU 与 vfio 绑卡排障
2026/10/28 20:20:50 网站建设 项目流程

CentOS 安装 DPDK 这件事,说难不难,说简单也真不简单。我第一次在机房那台老服务器上折腾的时候,光是大页内存和 IOMMU 两个坎就来回重启了四五次,最后发现真正卡住的居然是 BIOS 里一个没开的小开关。DPDK 也就是数据平面开发套件,它把网卡从内核手里抢过来,交给用户态程序直接轮询收包,绕开了中断、协议栈和内存拷贝这一整套开销,小包转发性能能甩开传统内核路径一大截。这篇文章面向的是需要在这套系统上跑高性能网络程序的人:做网关、做流量分析、做负载均衡、做虚拟化数据面的,或者单纯想学明白 DPDK 到底怎么落地的开发者。从系统底座准备、大页与直通配置、源码编译、网卡绑卡,一直到 testpmd 跑通和常见故障排查,我把自己踩过的坑和验证过的步骤都摊开讲一遍,你照着做基本能少走大半弯路。

1. 动手前先想清楚这三件事

在终端里敲第一条命令之前,有几个问题必须先有答案,否则后面大概率是白忙活。这一节不写代码,但比后面任何一节都重要——方向错了,编译得再顺利也跑不起来。

1.1 DPDK 到底解决了什么问题,什么时候不该用它

传统的内核收包路径是这样的:网卡收到包,触发硬件中断,CPU 跳进中断处理程序,分配 sk_buff 结构体,把数据从网卡环形缓冲区拷贝到内核内存,一路沿着协议栈往上走,经过路由查找、netfilter 钩子,最后拷贝到用户态 socket 的接收缓冲区。这一路上有中断上下文切换、有内存拷贝、有协议栈加锁,单核跑到几十万包每秒就基本顶天了,而且延迟抖动很大。

DPDK 的思路非常直接:既然这些开销都是内核协议栈带来的,那就不走内核。网卡驱动改成用户态实现,收包靠 CPU 主动轮询而不是等中断,内存用提前分配好的大页并且保证物理连续,核间通信改用无锁环形队列,线程绑死在固定 CPU 核上避免调度漂移。这一套组合拳打下来,单个 CPU 核心处理小包的能力可以到千万级,而且延迟非常稳定。

但这里有个非常关键的前提:轮询意味着 CPU 永远在跑满转,哪怕一个包都没有。我曾经见过有人在只有 4 核的机器上给 DPDK 分了 3 个核,结果系统上其他服务全部卡死。所以判断标准很明确:如果你是做专用的网络设备、流量处理节点、或者服务器上有大量空闲核心可以专门划出来,那 DPDK 是合适的;如果你是想给一个普通 Web 服务提提速,或者机器负载本来就很紧张,那就别碰它,内核的 XDP、AF_PACKET 零拷贝,甚至干脆升级网卡和调一下中断亲和性,性价比高得多。

还有一个容易被忽略的点:DPDK 接管网卡之后,这块网卡在操作系统里就"消失"了。ping 不通、抓不到包、ip a看不见,SSH 千万别走这块卡。这是新手最常见的翻车现场,我有一次远程操作,手一抖把管理口的 PCI 地址填进了绑定命令,然后就连不上了,只能等机房的人帮忙重启。

1.2 版本选型:系统版本和 DPDK 版本的对应关系

CentOS 这边的情况比较特殊,因为不同大版本的内核和工具链差异很大,选错组合会在编译阶段就卡死。我把常见的搭配整理成一张表,这张表是我自己在几台不同机器上实测过的结论:

系统版本默认内核默认 gcc推荐 DPDK 版本主要障碍
7.x3.104.8.519.11 / 20.11 LTSgcc 版本太低,需 devtoolset 升级
8.x / Stream 84.188.521.11 / 22.11 LTSmeson/ninja 版本偏旧,需 pip 升级
Stream 95.1411.x22.11 / 23.11基本无阻碍,注意 64KB 页与巨页配置

为什么 CentOS 7 要特别小心?因为 DPDK 从 19.11 之后开始要求编译器支持 C11 标准,而 4.8.5 这个版本差了不止一点半点。你直接在 7 上make一个 22.11,会看到一堆error: ‘for’ loop initial declarations are only allowed in C99 mode之类的报错,这不是代码有问题,是编译器太老。解决办法有两个:装 devtoolset 换个高版本 gcc,或者干脆选 19.11 这个长期支持版本。

至于 CentOS 8 及以上,主要问题在构建工具。系统仓库里的 meson 往往只有 0.49 或者 0.5x,而 DPDK 21.11 要求 meson ≥ 0.53,22.11 要求 ≥ 0.53.2,23.11 要求 ≥ 0.57。用pip3 install --upgrade meson ninja通常能解决,但要注意 pip 装的 meson 在/usr/local/bin下,如果 PATH 里/usr/bin优先,你调用的还是老版本,用which meson && meson --version确认一下,必要时hash -r刷新一下命令缓存。

另外提醒一句,选择 LTS 版本(偶数年份的 .11 版本)在稳定性上更靠谱,社区维护周期长,遇到问题也容易搜到答案。追新版本的意义不大,除非你需要某个新硬件或者新特性。

1.3 硬件与固件的前置检查清单

软件配置到位了,硬件不支持照样白搭。动手之前花十分钟做这几项检查,能省掉后面几个小时的抓瞎。

第一,确认网卡型号是否被 DPDK 支持。用lspci -nn | grep -i ethernet拿到厂商和设备 ID,然后去 DPDK 源码的drivers/net/目录下对一下有没有对应的驱动目录。老的 82599 系万兆卡走ixgbe,X 系列走i40e/ice, Mellanox 的卡比较特殊,走mlx5并且不需要绑定 vfio,直接用 rdma-core 提供的 verbs 接口,配置方式完全不同。买卡之前一定要查清楚。

第二,确认主板和 CPU 支持 IOMMU。这一步在 BIOS 里做,选项名字各家主板不一样,通常叫VT-d、IOMMU、Directed I/O,AMD 平台叫AMD-Vi或者IOMMU。这个开关不开,vfio-pci 就绑不上网卡,只能退回使用 igb_uio 或者开启不安全模式,性能和安全性都打折。我见过太多人卡在这里,编译全过、大页也配了,就是绑卡失败,最后发现是 BIOS 里那个选项还停在 Disabled。

第三,看清楚网卡插在哪个 CPU 插槽对应的 PCIe 槽位上。双路服务器上跨 NUMA 节点访问内存的延迟能差出 40% 以上,收包线程必须和网卡在同一个 NUMA 节点。用lspci -vv -s <BDF>看NUMA node字段,或者直接cat /sys/bus/pci/devices/0000:3b:00.0/numa_node,返回 -1 说明没识别到,返回 0 或 1 就是节点号。这个数字后面分配核和内存都要用到。

第四,检查网卡固件版本和当前的驱动情况。ethtool -i ethX能看到驱动名和固件版本,某些老固件在 DPDK 下会有兼容问题,特别是做 SR-IOV 的时候。同时看一眼这块卡现在有没有被 bond、bridge、VLAN 子接口占用,有的话要全部拆干净,ip link show和nmcli connection show都过一遍,NetworkManager 特别爱在重启后把网卡抢回去。

2. 系统底座准备:依赖、大页与内核参数

底座这一步做完,后面编译和运行才有基础。很多"跑不起来"的问题其实都是这一步埋的雷。我按依赖、大页、IOMMU、CPU 隔离四块来讲,顺序就是操作顺序。

2.1 编译依赖安装与工具链升级

先装基础开发工具和一串依赖库。这里不需要纠结每个包干什么用,缺哪个后面编译会报错,不如一次装齐:

yum groupinstall -y "Development Tools" yum install -y numactl numactl-devel kernel-devel kernel-headers \ libibverbs-devel libpcap-devel openssl-devel \ python3 python3-pip elfutils-libelf-devel

numactl-devel必须装,DPDK 里大量代码要调 libnuma 的接口做 NUMA 感知,缺了它编译会直接中断。libibverbs-devel是给 Mellanox 网卡用的,如果你只用普通以太网卡,理论上可以不装,但装了也没坏处,省得以后换卡再补。kernel-devel和kernel-headers的版本必须和当前运行的内核版本完全一致,否则编译内核模块(比如 igb_uio、kni)时会报错。装之前先uname -r看当前内核,再用rpm -qa | grep kernel-devel对比,对不上就装对应版本的。

CentOS 7 上还要处理 gcc 版本问题:

yum install -y centos-release-scl yum install -y devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils scl enable devtoolset-9 bash gcc --version # 确认变成 9.x

scl enable只对当前 shell 生效,退出就失效了。图省事可以写进/etc/profile.d/devtoolset.sh,内容是source /opt/rh/devtoolset-9/enable,这样每个新会话自动生效。但要注意这会影响到系统上所有编译行为,如果有人在同一台机器上编译别的老项目,可能会因为 glibc 符号版本问题出岔子,所以更稳妥的做法是只在编译 DPDK 的那个终端里临时启用。

meson 和 ninja 在 CentOS 8 及以上用 pip 装:

pip3 install --upgrade pip pip3 install --upgrade meson ninja which meson && meson --version

确认版本号满足目标 DPDK 的要求,达不到就继续升,或者换个更高版本的 DPDK 降级需求。这里有个小细节:pip 装的可执行文件在/usr/local/bin,而sudo默认的 secure_path 可能不包含这个目录,导致sudo meson找不到命令。要么用sudo env "PATH=$PATH" meson,要么干脆用 root 直接操作,别在编译这一步纠结权限。

2.2 大页内存:2MB 和 1GB 的取舍与数量计算

大页是 DPDK 的命根子。普通 4KB 页在做大量内存映射时 TLB 命中率极低,而且很难保证物理连续,网卡的 DMA 需要物理地址连续的大块内存,只有大页能满足。系统上默认就支持 2MB 大页(只要 CPU 有pse标志),1GB 大页则需要 CPU 支持pdpe1gb,用grep pdpe1gb /proc/cpuinfo确认一下,有输出才说明支持。

两种大页的选择逻辑很简单:2MB 大页灵活,1GB 大页高效但死板。2MB 大页可以运行时动态调整数量,适合开发和测试环境,改一下/proc/sys/vm/nr_hugepages就行;1GB 大页必须在系统启动时预留,运行期几乎不可能分配成功,因为要求物理内存里有连续的 1GB 空间,系统跑一段时间后内存碎片化,这个条件基本满足不了。所以生产环境一般用 1GB 大页,因为它 TLB 覆盖范围大、页表项少,性能更稳。

数量怎么算?我给的公式是:所需内存 = 大页数量 × 单页大小,再留 30% 余量。以 testpmd 双口、每口 2 个队列为例,默认每个 port 建一个 32768 个元素的 mbuf 池,每个 mbuf 数据区 2KB 左右,加上rte_mbuf结构体本身(约 128 字节)和头部预留空间,实际占用大约 80MB 到 120MB,双口加起来 250MB 以内。听起来 1 个 1GB 大页就够了?不行,还要算上 ring、内存池元数据、以及每个 socket 的对齐开销,更关键的是 DPDK 默认会预留一部分内存做各种池化,实际跑下来 1GB 大页建议至少给 2 个,2MB 大页建议给 1024 个(也就是 2GB)。

配置 1GB 大页写进 grub 参数,编辑/etc/default/grub,在GRUB_CMDLINE_LINUX里追加:

default_hugepagesz=1G hugepagesz=1G hugepages=4 hugepagesz=2M hugepages=1024

然后重新生成引导配置:

grub2-mkconfig -o /boot/grub2/grub.cfg # 传统 BIOS grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg # UEFI 启动

这里有个特别容易踩的坑:hugepages=4配在hugepagesz=1G后面,它的语义是每个 NUMA 节点各分配 4 个,不是总共 4 个。在双路服务器上,实际会吃掉 8GB 内存。我当初在一台 32GB 的机器上写了个hugepages=16,以为 16GB 够用,结果开机直接吃掉 32GB,系统差点没起来。所以配置前一定先numactl --hardware看清楚有几个节点,内存总量够不够。稳妥的做法是用hugepages=2起步,开机后用 sysfs 按节点精细调整。

开机后用下面的命令验证:

grep Huge /proc/meminfo cat /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/nr_hugepages cat /sys/devices/system/node/node1/hugepages/hugepages-1048576kB/nr_hugepages

HugePages_Total对得上才算成功。如果 1GB 大页数量为 0 但参数明明写了,八成是 CPU 不支持pdpe1gb,或者 BIOS 里某个内存映射相关的选项挡住了,这时候退回用 2MB 大页也能跑,只是性能略差。

2.3 IOMMU 开启与直通模式参数

IOMMU 的作用是给设备提供独立的地址翻译和访问隔离,vfio-pci 驱动依赖它才能安全地把网卡交给用户态。没有 IOMMU,只能靠 igb_uio 这种老办法,而 igb_uio 从 20.02 版本起已经被挪出主仓库,新版本要单独去拉代码编译,很折腾,而且没有 DMA 隔离,出问题容易整机崩溃。

开启步骤分两层。BIOS 层前面说过了,找到VT-d或IOMMU打开。内核层在 grub 参数里加:

intel_iommu=on iommu=pt

AMD 平台把intel_iommu换成amd_iommu。这里重点说iommu=pt这个参数,它的意思是 passthrough 模式,只对需要 IOMMU 的设备做地址翻译,其他设备走恒等映射,这样能避免全局开启 IOMMU 带来的性能损耗。生产环境我强烈建议加上,不加的话某些场景下整体 I/O 吞吐会掉几个百分点。

改完参数重启,验证:

dmesg | grep -e DMAR -e IOMMU | head -20 ls /sys/kernel/iommu_groups/ | wc -l

第一条命令应该能看到DMAR: IOMMU enabled之类的输出,第二条返回的组数量大于 0 就说明 IOMMU 正常工作了。如果组数是 0,回去检查 BIOS,别怀疑系统。

这里补充一个实战中很烦的问题:IOMMU 分组共享。某些主板的 PCIe 插槽没有 ACS 支持,导致多个设备被划到同一个 IOMMU 组里,vfio-pci 要求整组设备都绑定才能工作,而组里可能有你根本不能动的设备(比如板载 SATA 控制器),这就死锁了。判断方法是ls /sys/kernel/iommu_groups/*/devices/看每个组里有什么。真遇到了,换插槽是最干净的办法;开启 ACS override 内核补丁能绕过,但这等于拆掉了隔离墙,生产环境别干这种事。

2.4 CPU 隔离与 NUMA 绑定策略

DPDK 的轮询线程必须独占 CPU 核,否则被内核调度器挪走一下,收包队列就溢出了。所以要做 CPU 隔离,把准备给 DPDK 用的核从内核调度里摘出来。在 grub 参数里加:

isolcpus=2-9 nohz_full=2-9 rcu_nocbs=2-9

isolcpus把指定核从普通调度域里隔离,nohz_full让这些核上的时钟中断也尽量停掉(减少中断打扰),rcu_nocbs把 RCU 回调从这些核上迁走。三个参数配合使用效果最好。

有两个限制要记住:nohz_full不能包含 CPU0,写了会报错或者被内核忽略;隔离的核数不宜过多,一般留出 2 个核给系统本身用(一个给内核线程,一个给 SSH 和监控)。隔离 8 个核的机器,实际分给 DPDK 的通常是 6 个。

NUMA 绑定策略的规则就一句话:网卡在哪个节点,收包线程和大页内存就用哪个节点。假设lspci查到网卡在 node 0,那 EAL 参数里就用--lcores或者-l指定 node 0 上的核,同时用--socket-mem=1024,0把内存全部分给 socket 0。反过来配的话,每个包都要跨节点访问内存,延迟直接翻倍,这是性能调优里最容易被忽视、影响又最大的一项。

验证隔离是否生效:

cat /sys/devices/system/cpu/isolated tuned-adm profile # 如果有 tuned,确认没被它改回去

看到2-9就说明生效了。有些发行版装了tuned或者irqbalance服务,它们会自作主张改中断亲和性,建议给 DPDK 用的机器上直接把irqbalance停掉,systemctl disable --now irqbalance,省得它跟你的绑核策略打架。

3. 编译 DPDK:从源码到可用的库

底座通了,接下来把 DPDK 本身编出来。这一节分新旧两套构建系统讲,因为现实中这两种场景都会遇到。

3.1 获取源码与目录结构速览

源码从官方发布渠道拿,一般用.tar.xz的稳定版压缩包,比 git clone 下来再切 tag 省事,也避免了子模块拉取失败的问题:

wget https://fast.dpdk.org/rel/dpdk-22.11.tar.xz tar xf dpdk-22.11.tar.xz cd dpdk-stable-22.11.3

解压后先瞄一眼目录结构,知道东西在哪,后面查文档和改配置会方便很多。lib/下面是核心库,EAL、mempool、ring、mbuf 这些都在里面;drivers/是各类网卡和加解密设备的 PMD 驱动,drivers/net/下按厂商分目录;app/是 testpmd、dpdk-pdump 这些工具;examples/是示例程序,l2fwd、l3fwd 这些经典例子都在;usertools/里有那个非常好用的dpdk-devbind.py;doc/里是文档,遇到参数不明白直接grep源码里的文档比上网搜靠谱。

编译前先确认一下 CPU 支持的指令集,这个会影响后面的编译参数选择:

grep -o -m1 'avx512f\|avx2\|sse4_2' /proc/cpuinfo | sort -u

有 avx512f 说明是比较新的服务器平台,编译时开对应指令集能拿到更好的性能。但这一步要谨慎,后面会讲为什么。

3.2 meson + ninja 构建流程(新版本主线)

从 19.11 开始 meson 成了官方构建系统,流程非常清爽:

meson setup build --buildtype=release -Dexamples=l2fwd,l3fwd ninja -C build

--buildtype=release会带上-O3优化,别用默认的 debug 模式跑性能测试,那个版本慢得离谱,我见过有人拿 debug 版本测出来的数据去汇报,结果差了将近一倍。-Dexamples指定要一起编译的示例程序,不指定的话默认一个都不编,后面想跑 l2fwd 还得回头重新编,浪费一次十分钟。

编完之后安装到系统:

ninja -C build install ldconfig

默认装到/usr/local,头文件在/usr/local/include,库在/usr/local/lib64(64 位系统)。ldconfig这一步不能省,否则运行时会报找不到librte_eal.so。装完可以验证一下:

pkg-config --modversion libdpdk

能打印出版本号就说明安装到位了,新版本推荐用 pkg-config 来编译自己的程序,比老式的RTE_SDK环境变量清爽得多。

关于编译参数,有一个坑必须单独说:-Dplatform=native或者-Dcpu_instruction_set=native这类参数会让编译器针对当前这台机器的 CPU 生成指令。本机跑没问题,性能也确实好,但如果你编完之后把二进制拷到另一台稍老的机器上,就会直接Illegal instruction崩溃,连个像样的错误信息都没有。我吃过这个亏——在测试机上编好,部署到生产机,一启动就挂,查了俩小时才发现是 AVX512 指令不被支持。所以只有确定目标机器 CPU 型号完全一致时才用 native,否则老老实实用默认的 generic 配置,或者显式指定一个保守的指令集等级。

3.3 make 构建流程(老版本与特殊场景)

如果你用的是 19.11 或者更早的版本,构建系统还是老的 make,命令形式完全不同:

make install T=x86_64-native-linuxapp-gcc DESTDIR=install

T=后面是目标模板名,格式是架构-平台-编译器。编完之后产物在install/目录下,里面include、lib、bin、share结构跟系统目录一样,方便整体拷贝。这种构建方式编出来的东西自带RTE_SDK路径依赖,编译自己的程序需要设置环境变量:

export RTE_SDK=$(pwd) export RTE_TARGET=x86_64-native-linuxapp-gcc

至于内核模块,如果你确实需要用 igb_uio,得单独编:

make -C install/kernel

编出来是igb_uio.ko,insmod加载。加载前一定要确认当前内核版本和编译时的 kernel-devel 版本一致,不一致会报version magic不匹配,或者更糟——加载成功但运行时不定期崩溃,这种问题极难排查。所以模块加载完立刻dmesg | tail看一眼有没有异常输出。

说实话,新版本用 meson 之后这些麻烦基本都没了,除非有历史包袱,否则没必要守着老构建系统。

3.4 环境变量与工具脚本准备

DPDK 装好之后,日常操作最常用的是dpdk-devbind.py这个 Python 脚本,在源码的usertools/目录下。建议直接拷到/usr/local/bin或者加个软链接,省得每次都写长路径:

ln -s /path/to/dpdk/usertools/dpdk-devbind.py /usr/local/bin/dpdk-devbind chmod +x /usr/local/bin/dpdk-devbind dpdk-devbind --status

--status输出分两部分,上面是网卡列表,包含 PCI 地址、型号、当前绑定的驱动;下面是当前绑定的驱动模块和它们的兼容性。这一步的输出是后面绑卡操作的全部依据,务必看清楚。

如果还想跑 testpmd 这类工具,路径在build/app/dpdk-testpmd(新版)或者install/bin/testpmd(老版),也可以ninja -C build install之后直接敲dpdk-testpmd,因为装进了/usr/local/bin。顺手确认一下LD_LIBRARY_PATH有没有问题,理论上前一步ldconfig之后就无所谓了,但如果装到了非标准路径,记得 export 一下。

4. 网卡接管:vfio-pci 绑定全流程

绑卡是整篇文章里最容易出事的一步,因为它直接影响网络连通性。我按"先看、再解、后绑、最后固化"的顺序讲,每一步都有对应检查点。

4.1 为什么必须先卸载内核驱动

内核驱动和 vfio-pci 是互斥的,一块网卡同一时间只能被一个驱动接管。内核驱动在跑的时候会持续处理中断、更新统计、维护收发包队列,这时候你去绑 vfio,一定是失败或者行为异常。所以标准流程是先解绑、再绑定。

"解绑"具体包括三件事:第一,把网卡接口 down 掉,ip link set ethX down;第二,清掉这块卡上的所有配置,包括 IP 地址、VLAN 子接口、bridge 从属关系;第三,如果用了 NetworkManager,把对应的连接配置删掉,否则重启后它会把网卡抢回去,nmcli connection delete <name>。

清理干净之后,用ip addr show和ip link show再确认一次,这块卡应该显示DOWN并且没有 IP。然后dpdk-devbind --status看它是不是还挂在原来的内核驱动上(比如ixgbe、i40e),是的话就进入下一步。

注意:绑定前务必确认这块网卡的 PCI 地址不是你的管理口。远程操作的话,建议先在本地终端或者 BMC 带外管理里把命令准备好,绑错了还能救回来。我见过有人把板载千兆管理口绑了,然后只能抱着键盘去机房。

4.2 绑定操作的标准流程与命令

完整的绑定流程长这样,假设要绑两张网卡,PCI 地址分别是0000:3b:00.0和0000:3b:00.1:

# 1. 加载 vfio-pci 模块 modprobe vfio-pci modprobe vfio # 2. 检查 IOMMU 组权限 ls -l /dev/vfio/ # 3. 解绑内核驱动并绑定到 vfio-pci dpdk-devbind --bind=vfio-pci 0000:3b:00.0 0000:3b:00.1 # 4. 验证绑定结果 dpdk-devbind --status

绑定成功的标志是在--status输出里,这两张卡的drv=vfio-pci,后面的unused=字段显示*Active*。同时ls /sys/bus/pci/devices/0000:3b:00.0/driver应该指向vfio-pci。

/dev/vfio/目录下会出现若干编号的文件,每个编号对应一个 IOMMU 组,绑定成功后组里会多出对应的设备文件。如果你用普通用户跑 DPDK 而不是 root,需要调整这些文件的权限,简单粗暴的做法是加 udev 规则或者直接chmod 660加个组。生产环境建议用 udev 规则固化,别每次手工改。

绑定失败的常见表现有两种。第一种是Error: bind failed for ... Cannot open /sys/bus/pci/drivers/vfio-pci/new_id: No such file or directory,说明 vfio-pci 模块没加载成功,检查lsmod | grep vfio。第二种是Permission denied,通常是 IOMMU 组被其他设备占用,回去看 2.3 节讲的组共享问题。

还有一种特殊情况:机器上 IOMMU 确实开不了(老主板或者虚拟机环境),可以用不安全模式强行绑定:

# 仅在确认 IOMMU 无法开启时使用 modprobe vfio-pci enable_unsafe_noiommu_mode=1

这个模式放弃了 DMA 隔离,性能上也会有损失,等于把安全性让出去了。能用 IOMMU 就别用这个,只作为无路可走时的兜底手段。

4.3 重启后的状态恢复与自动化

这是个非常实际的运维问题:绑卡状态不会自动保存,重启之后网卡会回到内核驱动手里。如果你是手工绑的,每次重启都要重新执行一遍,忘一次就导致业务起不来。

最省事的办法是写个 systemd 服务,开机自动执行绑定。写一个脚本/usr/local/sbin/dpdk-bind.sh,把绑卡命令放进去,然后建一个 unit 文件:

[Unit] Description=Bind NICs to vfio-pci for DPDK After=network.target [Service] Type=oneshot ExecStart=/usr/local/sbin/dpdk-bind.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target

用systemctl enable dpdk-bind.service启用。脚本里建议加上判断逻辑:先检查是否已经绑定,已绑定就跳过;然后ip link set down,再执行绑定;最后打印一下状态方便查日志。

注意:PCI 地址在某些主板上不是固定的,换个插槽或者调整 BIOS 设置后可能变化。稳妥的写法是用driver_override机制,或者写脚本时先用lspci按设备 ID 匹配再取地址,别把0000:3b:00.0这种硬编码当成永恒真理。我在一台机器上就因为换了个 PCIe 插槽,所有自动化脚本全部失效,排查了半天才发现是地址从3b变成了5e。

另外,如果你的场景需要临时把网卡还给内核(比如要抓个包、或者跑个普通测试),反向操作就是dpdk-devbind --bind=<原驱动名> <PCI地址>,比如--bind=ixgbe 0000:3b:00.0,绑回去之后ip link set up就能用。这个操作熟悉一下很有用,别把网卡永久锁死在 DPDK 模式,调试的时候会很难受。

5. 跑通第一个例子:testpmd 与 l2fwd

编译装好、网卡绑好、大页也有了,是骡子是马拉出来遛溜。这一步的目标是确认整条链路是通的,顺便把常用参数摸熟。

5.1 testpmd 交互式验证与关键参数

testpmd 是 DPDK 自带的测试工具,功能最全,也最适合做初始验证。最基本的启动命令:

dpdk-testpmd -l 2-5 -n 4 \ -a 0000:3b:00.0 -a 0000:3b:00.1 \ --socket-mem=1024,0 \ -- --forward-mode=io --txq=2 --rxq=2 --burst=64

逐个参数解释一下,这些参数是后面所有 DPDK 程序都要写的,务必搞明白:

-l 2-5指定使用 2 到 5 这四个逻辑核,第一个核干管理活,后面三个干收发包。这个范围必须落在前面isolcpus隔离的范围内,否则性能会抖。

-n 4是老式参数,指定内存通道数,现代主板上这个参数已经不起作用了,新版 EAL 会直接忽略(可能会打一行 deprecated 警告),写不写都行,但老脚本里常见,看到别慌。

-a <PCI地址>是设备白名单,只接管指定的网卡。如果不写-a也没有--no-huge之类的参数,EAL 会尝试接管所有它能接管的设备,这在多网卡的机器上是灾难。养成写-a的好习惯。

--socket-mem=1024,0表示 socket 0 分配 1024MB,socket 1 分配 0。这个和 NUMA 绑定强相关,网卡在哪个节点,内存就给哪个节点。写成,0就是把另一个节点饿死,避免内存被分散分配。

--之后是 testpmd 自己的参数,--forward-mode=io是收进来再发出去的模式,--txq/--rxq指定队列数,队列数一般跟绑定的核数匹配,4 个核减去 1 个管理核,3 个工作核,配 2 个队列比较合理。

启动成功的话会看到一大片初始化日志,最后停在testpmd>提示符。这时候敲:

start show port stats all

就能看到每个端口的收发计数在跳。如果RX-packets一直是 0,先别怀疑程序,检查网线插没插、对端有没有发流、光模块亮不亮,这些物理层问题比软件问题常见得多。

退出用stop然后quit。如果启动阶段就报错,往下看第 6 节的问题速查。

5.2 l2fwd 双向转发实测与观察

testpmd 验证通过之后,可以跑 l2fwd 做个更接近真实场景的测试。l2fwd 干的事很简单:从 0 口收到的包,把源和目的 MAC 交换一下,从 1 口发出去,反向同理。这个例子小到能一眼看懂代码,但完整展示了 DPDK 程序的骨架——初始化 EAL、配置端口、建内存池、起循环。

如果编译时加了-Dexamples=l2fwd,二进制在build/examples/dpdk-l2fwd,直接跑:

./build/examples/dpdk-l2fwd -l 2-5 -n 4 \ -a 0000:3b:00.0 -a 0000:3b:00.1 \ --socket-mem=1024,0 -- -p 0x3

-p 0x3是端口掩码,二进制11表示启用 0 号和 1 号端口。这个东西第一次见容易懵,其实就是位运算,第 n 位为 1 表示启用第 n 个端口。

跑起来之后会每隔一段时间打印统计,重点看两列:Pkts rx/tx和Missed。正常情况下 rx 和 tx 应该基本相等,差太多说明有丢包,Missed为 0 或者很小。如果Missed一直涨,说明 mbuf 池不够用了,加大池子或者减少队列数。

测试流量怎么造?最简单的办法是拿另一台机器用pktgen或者iperf打流。如果只有一台机器,可以起两个 testpmd 实例,一个发包一个收包,用--file-prefix区分共享内存(这个参数非常重要,同一台机器跑多个 DPDK 实例必须加,否则会抢同一块大页内存报错)。

实测下来,在双口万兆卡、4 个核、2KB 小包的条件下,l2fwd 的转发能力大概能到线速的七八成,瓶颈通常在 PCIe 带宽或者单核处理能力上。换个 4 队列、6 个工作核,基本就能打满线速。这个数据供你做个参考,具体环境差异很大。

5.3 基础性能调优参数与验证方法

跑通之后如果性能不理想,有这几个参数值得调,按见效快慢排序:

描述符数量:--rxd和--txd,默认各 128(有的版本是 256)。加大到 512 或 1024 能吸收更多突发流量,代价是内存占用增加、延迟略微上升。我的经验是收包侧给 512,发包侧给 512,比较均衡。

突发大小:--burst默认 32,这个值影响一次循环处理多少个包。太小了循环开销占比高,太大了延迟增加且缓存不友好。实测 32 到 64 之间比较合适,超过 128 收益递减明显。

mbuf 数量:通过--total-num-mbufs或者改代码里的池大小控制。计算方式是每个队列需要rxd + burst个发送侧缓存,再乘上队列数,最后留一倍余量。以 2 口 2 队列为例,512 描述符的话,一个端口大概要 2 × (512 + 64) × 2 ≈ 2300 个,池子给 8192 绰绰有余。

验证调优有没有效果,别只看程序自己打的数字。用sar -n DEV 1看物理口的实际流量,同时在交换机侧看端口计数,两边对得上才是真的。程序里统计的 rx/tx 是网卡驱动的软件计数,如果 DMA 出了岔子,软件计数和物理层对不上,这种情况在下面排查章节里会提到。

6. 常见问题与排查实录

这一节写的是我在实际环境里真真切切遇到过的故障,按症状、原因、处理方式整理。有些问题看着像玄学,拆开看其实都是配置没对齐。

6.1 大页内存相关的典型故障

大页这一块的报错信息很有辨识度,我整理成一张表:

报错信息根本原因处理方式
No free hugepages reported in hugepages-1048576kB1GB 大页数量为 0检查 grub 参数、CPU 是否支持 pdpe1gb、是否重启生效
Cannot get hugepage information挂载点不存在或权限不足mount -t hugetlbfs nodev /dev/hugepages
Not enough hugepages available数量不够或全被别的实例占了加大数量,或--file-prefix区分实例
Hugepage allocation failed物理内存碎片化重启后立即分配,别等系统跑久了再建

第一个是最常见的。参数明明写在 grub 里,但/proc/meminfo里数量就是 0,八成是grub2-mkconfig生成了配置但没写进启动分区,或者机器有多个内核,改的那份是另一份配置。用cat /proc/cmdline看实际生效的启动参数,里面没有hugepagesz=1G就说明没生效,回去重新生成配置,注意 UEFI 和传统 BIOS 的路径不一样。

最后一个也值得展开说。1GB 大页要求物理连续内存,系统启动了几天,内存被各种进程和缓存切得七零八落,这时候想再分配 1GB 连续块基本没戏。有个小技巧:如果大页分配失败,可以先echo 3 > /proc/sys/vm/drop_caches清一下页缓存,再试一次,有时候能成功。但根治办法还是提前在启动参数里预留,别指望运行期动态调整。

实操心得:给 DPDK 用的机器,我习惯在开机脚本里就做一次大页可用性检查,数量不对就立刻报警。因为这个问题不会在编译阶段暴露,往往等到程序跑起来才报错,那时候排查成本高得多。

6.2 网卡绑定与设备探测失败排查

绑定环节的故障种类多,但都有一个共同点:报错信息里会明确提到是哪个设备、什么操作失败了。学会读这几行输出,能省下大量搜索时间。

症状一:绑定命令返回成功,但--status里显示还是原来的驱动。这通常是内核驱动在自动抢回设备。NetworkManager、udev 规则、或者某些厂商装的工具包会自动把网卡重新绑定。处理方式是先停掉这些服务再绑,或者加一条 udev 规则永久屏蔽。检查dmesg | tail -30能看到驱动反复加载卸载的痕迹。

症状二:testpmd 启动时报No probed ethernet devices。说明 EAL 一个网卡都没接管到。三种可能:-a参数里的 PCI 地址写错了;网卡还绑在内核驱动上没绑 vfio;或者 vfio 模块没加载。逐一排查,dpdk-devbind --status是第一步。

症状三:绑定时报Device is already bound to another driver。之前绑过一次没解干净,先进/sys/bus/pci/drivers/<驱动名>/unbind手工解绑,echo <PCI地址> > unbind即可,然后再走正常流程。

症状四:程序跑起来但收不到包。这个最磨人。先确认链路灯亮、对端有流量;然后看网卡缓存统计,show port stats all里的imissed如果一直涨,说明硬件收到了但软件没取,通常是 CPU 核被抢占或者大页不够;如果rx-packets完全是 0 而对面确实在发,检查一下是不是 VLAN 标签被网卡硬件剥掉了但程序没配对应的过滤规则。我遇到过一次,折腾半天,最后发现是交换机端口配置了 trunk,普通 untagged 包直接扔了。

6.3 编译与运行期问题速查表

跨整个流程的高频问题汇总,遇到对号入座:

阶段症状可能原因处理
编译for loop initial declarations报错gcc 版本 < 4.9devtoolset 升级,或降级 DPDK 版本
编译meson: command not found未安装或 PATH 不对pip 安装并检查which meson
编译ninja: error: unknown targetmeson setup 没成功删 build 目录重新 setup,看首次报错
编译rte_config.h: No such file头文件路径没配用 pkg-config 拿 cflags
运行Illegal instruction编译时用了 native 优化用通用指令集重新编译
运行Cannot init EAL大页、权限、IOMMU 三者之一按 6.1、6.2 逐项排查
运行Failed to create mbuf pool内存不够或池大小写错加大 socket-mem,检查池参数
运行收包速率远低于预期核未隔离、NUMA 跨节点检查 isolcpus、socket-mem 配置
运行多个实例冲突共享内存文件名撞了每个实例加不同--file-prefix

Illegal instruction那个我要再强调一次,因为它太迷惑人了。编译的时候一切正常,一运行就崩,没有堆栈没有提示,新手往往以为是硬件问题。实际上就是那台机器的 CPU 不支持编译时选定的指令集。解决办法是用-Dcpu_instruction_set=generic重新编译,或者确认部署环境的 CPU 型号后选一个都支持的等级。

还有个小问题顺带提一句:某些机器上/dev/hugepages挂载是临时的手工挂载,重启就没了,程序启动报Cannot get hugepage information。正确的做法是写进/etc/fstab,nodev /dev/hugepages hugetlbfs defaults 0 0,一劳永逸。

6.4 几个用血换来的实操心得

最后分享几条文档里不会写、但实测非常有用的经验,都是我踩过之后才明白的。

第一条:先跑通最简配置,再逐步加参数。我见过太多人一上来就把队列数、描述符、绑核参数全堆满,跑不起来之后完全不知道是哪个参数的问题。正确的顺序是:单口、单队列、最少的大页、不隔离 CPU,先确认能收包;然后加第二个口,加队列,加大页,加隔离。每一步都验证一次,出问题立刻就知道是刚加的那一项引起的。这个笨办法能把排查时间压缩到最短。

第二条:监控要提前做,别等出问题。DPDK 程序把网卡抢走了,系统自带的监控工具全部失灵,ifconfig、sar -n DEV都看不到这块卡。我一般会在程序里加个定时打印,把rx/tx/imissed/ierrors四个关键计数打出来,或者用dpdk-pdump抓包(这个工具通过 ring 把包镜像出来,不中断主程序)。没有监控的情况下,一旦性能下降,你连是不是网卡的问题都判断不了。

第三条:隔离核之后,记得调整进程的调度策略。隔离只是让内核不去调度普通任务,已经跑在这几个核上的线程不会被自动赶走。可以在程序里把线程属性设成 SCHED_FIFO 实时调度(需要 root 权限),配合mlockall锁定内存防止换页,能明显减少延迟毛刺。代价是配置错了可能导致 shell 卡死,所以要留一个没隔离的核做救援通道。

第四条:记录每一次成功的配置。PCI 地址、大页数量、绑核范围、编译参数、固件版本,全部写成一份文档存起来。换机器部署的时候照着抄,能省掉 90% 的重复摸索。我现在每个部署过的环境都有一份deploy.md,里面连 BIOS 里哪些选项改了都记着,这个习惯救过我不止一次。

这套流程走下来,一台干净的机器从装系统到 testpmd 出包,熟练的话半小时以内能搞定。难的不是命令本身,是每一步背后的原理——为什么这里要 1GB 大页不用 2MB,为什么那个参数必须加,为什么绑卡失败要去看 IOMMU 分组。想明白了这些,遇到没见过的报错也能顺着逻辑往下推,而不是到处搜关键词碰运气。真正上手之后你会发现,最花时间的往往不是 DPDK 本身,而是硬件环境那一堆看不见的前提条件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询