ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CentOS 部署 DPDK:大页、IOMMU、VFIO 绑卡与 l2fwd 排障

CentOS 部署 DPDK:大页、IOMMU、VFIO 绑卡与 l2fwd 排障

在 CentOS 上装 DPDK,说简单也简单,源码拉下来,一条meson一条ninja,几分钟就能编完;说麻烦也真麻烦,真正把大多数人卡住的从来不是编译,而是大页内存没配对、IOMMU 没开、网卡绑上去之后 ssh 直接断线、testpmd起不来还只丢给你一句Cannot get hugepage information。这篇东西我按自己在一台物理机上从零到跑通l2fwd的完整路径来写,中间穿插编译参数的取舍、大页数量的算法、绑卡失败的各种排查,以及几个文档里基本不会写、但踩一次能记一辈子的细节。不管你是刚接触高性能网络转发的新手,还是想给自家采集/网关程序换一套数据面底座的老手,应该都能从里面挑到能直接抄的东西。前提是你要清楚:DPDK 不是装个软件包就能用的东西,它要拿走物理网卡的独占权,要一块锁死的物理内存,还要独占若干个 CPU 核,装之前最好心里有数。

1. 先想清楚:为什么要在 CentOS 上装 DPDK

1.1 DPDK 到底替你省掉了什么

很多人对 DPDK 的第一印象是“跑得快的网络库”,这说法不太准确。它本质上是把数据包从网卡到应用程序之间那条路上,所有由内核代劳的环节全部删掉。传统路径是网卡收包、DMA 到内核环形缓冲区、触发硬中断、软中断走协议栈、再通过 socket 拷贝到用户态,一个包要经历好几次内存拷贝和上下文切换;DPDK 的做法是驱动改成纯轮询(PMD,Poll Mode Driver),网卡收到的包直接 DMA 到用户态预先分配好的大页内存池里,CPU 死循环去查描述符,查到就处理,全程零拷贝、零中断、无系统调用。代价也直接摆在明面上:那个核被 100% 占用,哪怕一个包都没有,它也在一刻不停地转。

所以它换来的不是“平均性能提升”,而是延迟的确定性和吞吐的上限。我实测过一个很朴素的对比场景,同样的机器、同样的千兆口,纯内核转发大概跑到线速的六成多就开始丢包,抖动肉眼可见;换成 DPDK 的l2fwd之后,小包也能贴着线速跑,包间隔基本是平的。这也是为什么这类技术最终都落在了对延迟和抖动极其敏感的位置上——流量采集探针、软负载均衡、协议解析网关、以及各种需要自己拼包拆包的高性能转发程序。

理解了这一层,你才会明白后面每一个配置步骤在干什么:大页是为了让 DMA 拿到的物理地址连续且不被换出,绑核是为了让轮询线程不被调度器挪走,网卡绑定是为了让内核驱动彻底撒手,把设备交给用户态。这些都跟“装个库”没关系,是在给一个用户态驱动腾地方。

1.2 哪些场景值得上,哪些是真没必要

我见过太多人是在一个根本不合适的场景里硬上 DPDK,最后折腾两周,性能还不如原来。把判断标准说白了就三条:你的包速率是不是真的到了一个核处理不过来、你的延迟要求是不是已经敏感到微秒级、你的程序是不是能接受独占资源和独占网卡。

如果只是普通业务服务跑在几十兆带宽上,或者你压根不需要自己处理二层以下的东西,那用内核协议栈配合一些常规优化就够了,上 DPDK 属于用高射炮打蚊子。反过来,如果你在做的东西需要自己决定每个字节怎么拼、需要在收到包的同一时刻就做出转发决策、或者单机需要扛住几十万甚至上百万 PPS,那这层抽象带来的开销就变成主要矛盾了,这时候 DPDK 才真正划算。

还有一个容易被忽略的点:DPDK 拿走的网卡,内核就看不见了。这意味着ip addr、tcpdump、iptables在这个口上全部失效,你的管理通道、监控采集、日志回传都得另外规划。我一般会在一台机器上留一个千兆管理口给系统用,其余的万兆/25G 口全部贡献给数据面,这个物理上的隔离比任何软件配置都管用。

1.3 开工前必须先确认的四件事

动手之前有几项信息必须先摸清楚,否则后面每一步都可能白费。

第一是网卡的芯片型号。DPDK 对网卡的支持是非常具体的,不是“支持 Intel 网卡”这种笼统说法,而是精确到某几个控制器系列。用lspci -nn | grep -i ethernet看设备 ID,然后去对照当前版本的支持列表,确认它有对应的 PMD。虚拟机的 virtio 网卡和很多消费级网卡基本没戏,服务器上的千兆 I350、万兆 82599/X540/X710、25G 的 E810 这些才是常客。

第二是 CPU 的核心数和大页内存的可用量。DPDK 至少要给你一个核做轮询,实际部署里通常要 2 到 8 个核,加上一个管理核,机器核数太少就没什么意思了。

第三是虚拟化环境还是物理机。云主机和虚拟机里做实验可以,但 IOMMU 直通往往不可用,只能退回--no-huge或者enable_unsafe_noiommu_mode这种降级玩法,性能和稳定性都打折扣,只适合验证流程。

第四是你的系统版本。这一点决定了后面用哪套构建工具,是十几年前那套make install T=...,还是现在的meson+ninja。老系统上还要额外考虑编译器版本够不够新这个问题,具体下面会展开。

2. 环境准备:把坑提前填平

2.1 内核版本与头文件的对齐

装 DPDK 最隐蔽的一个坑,是内核头和当前运行内核版本对不上。DPDK 编译用户态程序的同时会去编内核模块(早期的igb_uio、kni之类),这些东西依赖/lib/modules/$(uname -r)/build这个路径下的内核源码树。CentOS 上默认装好的系统,这个目录经常是空的,或者指向一个比你当前内核新的版本,编出来的模块自然装不进去。

所以第一步永远是确认三件套一致:uname -r输出的版本、rpm -q kernel-devel kernel-headers装上的版本、以及/usr/src/kernels/下实际存在的目录名。三者不一致的话,直接yum install kernel-devel-$(uname -r) kernel-headers-$(uname -r)精确对齐,装不了说明当前内核版本已经不在仓库里了,那就老老实实重启进新内核,别在这上面硬扛。

顺带一提,如果你的机器上跑着动态加载的内核模块(比如某些存储或网卡驱动是 DKMS 编的),升级内核之前要先确认它们能跟上,不然重启之后机器起不来,那才是真的麻烦。

uname -r rpm -q kernel-devel kernel-headers ls -l /usr/src/kernels/

三条命令输出对不上就是有问题,别抱侥幸心理往下走。

2.2 编译依赖的完整清单

依赖这块,网上的教程经常只给你三五条,编到一半才发现缺这个缺那个。我按“必装”和“看情况装”两类整理了一份,CentOS 7 用yum,8 和 Stream 用dnf,包名基本通用。

必装的:gcc、gcc-c++、make、git、meson、ninja-build、numactl-devel、kernel-devel、kernel-headers、python3、python3-pip、libbsd-devel、zlib-devel、elfutils-libelf-devel、libpcap-devel。

看情况装的:要用 RDMA 相关功能就得加rdma-core-devel、libibverbs-devel、librdmacm-devel;要开加密相关的功能加openssl-devel;要编 BPF 相关的东西加libbpf-devel、dwarves(提供pahole);做流量过滤想用 netlink 加libmnl-devel;需要压缩功能加libarchive-devel。

这里有个细节值得说清楚:meson在 CentOS 7 的默认仓库里版本老得离谱,很多新版本 DPDK 要求的meson >= 0.53根本满足不了,正确姿势是用 pip 装。而ninja-build这个包在 CentOS 上永远叫ninja-build,不叫ninja,写错了会卡很久。另外 CentOS 7 自带的 GCC 4.8.5 编不动较新的 DPDK,需要装devtoolset-9或devtoolset-10套件,然后scl enable devtoolset-9 bash切进去再编。这一步如果不做,编译报错会从meson.build里冒出来,看上去完全莫名其妙。

注意:scl enable只在当前 shell 生效,meson setup和ninja必须在同一个 shell 会话里执行,中途断开重连就会回到旧编译器,报出一堆前后矛盾的错。

2.3 大页内存与 IOMMU 的前置配置

大页内存是 DPDK 的地基,配错了后面全是白搭。原理不复杂:普通内存页是 4KB,页表项多、TLB 命中率低,而且可能被内核换出或迁移;大页通常是 2MB 或 1GB,页表项少得多,而且可以预先锁定在物理内存里,保证网卡 DMA 拿到的地址稳定可控。

2MB 页和 1GB 页怎么选?简单说,小页配置灵活、不需要重启、随时可调,适合调试和小规模;1GB 页必须在启动参数里预留,配置完要重启,但页表开销小、更稳定,生产环境用得多。我自己的习惯是:开发调试机器用 2MB 页,几十个就够;真正部署用 1GB 页,按需预留。

2MB 页的分配很直接:

# 预留 2048 个 2MB 页,合计 4GB echo 2048 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages # 验证 grep Huge /proc/meminfo

HugePages_Total和HugePages_Free应该和你写进去的数字一致。如果写进去之后 Total 没变,多半是内存不够或者被碎片化了,这种情况在长时间运行的机器上很常见,解决办法是重启,或者在启动参数里预留。

1GB 页要走 GRUB 启动参数,编辑/etc/default/grub,在GRUB_CMDLINE_LINUX里追加:

default_hugepagesz=1G hugepagesz=1G hugepages=8 intel_iommu=on iommu=pt

这里hugepages=8意味着预留 8GB 大页,写之前先算清楚机器内存够不够,预留过多会导致系统起不来或者内存告急。intel_iommu=on打开 IOMMU,iommu=pt是透传模式,能让没被 DPDK 接管、仍走内核驱动的设备性能不受影响。改完执行grub2-mkconfig -o /boot/grub2/grub.cfg(老版本是/boot/grub2/grub.cfg,UEFI 启动可能是/boot/efi/EFI/centos/grub.cfg)然后重启。

注意:intel_iommu=on是 Intel 平台的关键字,AMD 平台对应的是amd_iommu=on,写混了不会生效,只会安静地什么都不做。

IOMMU 打开之后还需要加载两个模块,vfio-pci和vfio_iommu_type1,并且确认它们在启动时自动加载,否则每次重启都要手动modprobe,很容易忘。另外还要把 IOMMU 分组模式调成直通模式,这行iommu=pt其实已经办了,但值得单独强调的是:如果不加,有些机器上多个设备会被分到同一个 IOMMU 组,而 DPDK 要求绑定的网卡独占一个组,否则报group is not viable,这时候要么换插槽让设备分开,要么打开 ACS(Access Control Services),前者靠硬件布局,后者靠 BIOS 里翻。

大页配好之后还要挂载 hugetlbfs,这个 CentOS 一般默认就有:

mount | grep huge # 输出应有 /dev/hugepages type hugetlbfs

没有的话补一条mount -t hugetlbfs nodev /dev/hugepages,并写进/etc/fstab。

3. 编译安装:从源码到环境就绪

3.1 版本选择与源码获取

版本选择上,我建议直接用 LTS 长期支持分支,别去追最新版。原因很实在:LTS 分支的 API 更稳定,社区里能搜到的资料多,很多第三方程序也是对着 LTS 版本编译的,用最新版反而会因为接口变动踩到一堆别人没踩过的坑。

从包里拿源码:

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

注意从 20.11 开始,DPDK 官方已经彻底移除了make构建体系,只剩meson+ninja。如果你看到的教程里还是make config T=x86_64-native-linux-gcc、make install T=...这一套,那是给 19.11 及更早版本的,硬套到新版本上会直接报“no rule to make target”。这个问题非常高频,看到老教程先确认版本。

源码目录的结构也要有点概念,后面找文件方便:lib/是各种库(librte_eal是核心环境抽象层,librte_ethdev是网卡抽象,librte_mbuf是包缓冲),drivers/分net/、crypto/、event/等子目录,app/下面是testpmd等工具,examples/是各种示例程序。搞不清一个功能在哪,先往这两个目录里翻。

3.2 meson 构建参数怎么定

构建分两步:meson setup生成构建目录并探测依赖,ninja执行编译。第一步的参数最关键,这里展开讲几个我常用的。

meson setup build \ --prefix=/usr/local \ --buildtype=release \ -Dexamples=l2fwd,l3fwd,testpmd \ -Dplatform=native

--buildtype=release是必须的,默认是debugoptimized,编出来的东西性能差一大截,很多人跑完发现性能不如预期,查了半天才发现是编译选项的问题。-Dexamples=指定要编哪些示例程序,不指定的话一个都不编,跑l2fwd的时候找不到可执行文件,第一反应会以为自己装错了。--prefix决定ninja install之后装到哪,默认是/usr/local,装到系统目录的好处是不用配PKG_CONFIG_PATH和LD_LIBRARY_PATH,坏处是有时候会和系统包管理打架,看你习惯。

-Dplatform=native会让编译器用-march=native,针对当前 CPU 生成最优指令,在只有同型号机器的环境里很香,但如果编好的二进制要复制到别的机器上跑,务必改成generic,否则会出现非法指令错误。

还有些常用的:

参数作用什么时候用
-Ddisable_drivers=net/ixgbe关掉不需要的驱动加快编译、减小体积
-Dmax_lcores=32限制最大 lcore 数减小静态数组占用,跑小规模时有用
-Ddefault_library=static只编静态库部署时不想管动态库路径
-Denable_kmods=true编内核模块需要kni等模块时
-Dtests=false跳过单元测试加快编译

编之前最好跑一下meson setup的依赖检查,看输出里Run-time dependency xxx found: NO有没有关键项。libnuma找不到是常见问题,说明numactl-devel没装,装完要rm -rf build重新 setup,因为 meson 会缓存探测结果。

编的时候用:

cd build ninja

核多的话可以ninja -j$(nproc),但注意内存,-j开太大在内存小的机器上会被 OOM killer 干掉,表现是编译中途莫名退出,dmesg里能看到 OOM 记录。我一般给-j的值不超过内存 GB 数除以 2。

3.3 安装与环境变量配置

ninja install ldconfig

装完之后,动态库在/usr/local/lib64(64 位系统),头文件在/usr/local/include,pkg-config文件在/usr/local/lib64/pkgconfig。如果你不装到系统目录,或者系统里同时存在多个版本,就需要手动配:

export PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig:$PKG_CONFIG_PATH export LD_LIBRARY_PATH=/usr/local/lib64:$LD_LIBRARY_PATH

验证装好没有:

pkg-config --modversion libdpdk

能打出正确版本号就说明pkg-config这条路通了。用gcc编自己的程序时,pkg-config --cflags --libs libdpdk一次性给出所有编译链接参数,非常省事:

gcc -O3 -o myapp myapp.c $(pkg-config --cflags --libs libdpdk)

如果这里报Package libdpdk was not found,九成是PKG_CONFIG_PATH没设对,先find / -name "libdpdk.pc"定位一下实际路径再改。

工具脚本在build/usertools/下,主要用dpdk-devbind.py,可以软链到/usr/local/bin方便调用:

ln -sf /usr/local/share/dpdk/usertools/dpdk-devbind.py /usr/local/bin/

不同版本这个脚本的位置有差异,装完之后find / -name "dpdk-devbind.py"找一下就行,别照着教程硬抄路径。

4. 网卡绑定与第一个程序跑通

4.1 用 devbind 绑定网卡的正确姿势

绑卡是整个流程里最危险的一步:绑错口,ssh 立刻断,机器直接失联。所以第一件事是搞清楚哪个口是管理口,把它用ip link和lspci明确对应起来,写在纸上都行。

# 看当前网卡状态和驱动 dpdk-devbind.py --status # 看设备位置 lspci | grep -i ethernet

--status的输出会分成几块:Network devices using kernel driver是还在内核手里的,Using DPDK-compatible driver是已经被 DPDK 接管的,Other network devices是没有可用驱动的。绑定就是把设备从第一类搬到第二类。

# 先停掉网卡,避免正在处理包的时候被抢走 ip link set enp3s0f0 down # 绑到 vfio-pci dpdk-devbind.py -b vfio-pci 0000:03:00.0 # 或者老办法,绑到 igb_uio dpdk-devbind.py -b igb_uio 0000:03:00.0

这里必须说清楚vfio-pci和igb_uio的取舍。igb_uio是早期的自研方案,配置简单,不需要 IOMMU,但它是内核树外模块,从较新的内核版本开始已经不再提供,而且在安全隔离性上不如 VFIO 彻底。vfio-pci是内核原生的虚拟化设备直通框架,有 IOMMU 保护,安全性好,是现在唯一推荐的方式。如果你的内核比较新,igb_uio大概率编不出来了,这不是你操作错了,是它就退役了。

没有 IOMMU 的机器(比如虚拟机里)想用 VFIO,可以打开非安全模式:

modprobe vfio enable_unsafe_noiommu_mode=1 echo 1 > /sys/module/vfio/parameters/enable_unsafe_noiommu_mode

名字里带unsafe就是字面意思,别在生产环境这么干。

绑卡还有一个持久性问题:默认情况下重启之后驱动归属会丢,得重新绑。想让它固定下来,可以给设备设置driver_override:

echo vfio-pci > /sys/bus/pci/devices/0000:03:00.0/driver_override echo 0000:03:00.0 > /sys/bus/pci/drivers/vfio-pci/bind

或者用 udev 规则固化,这样重启自动生效,不用每次手动敲。

注意:绑卡之前一定要确认这个口上没有承载任何管理流量。我见过有人在双口网卡上一口气把两个口都绑了,其中一个正是 ssh 的入口,绑完当场失联,最后只能到机房接显示器救回来。

4.2 大页检查与 testpmd 实测

绑好卡之后先别急着跑程序,确认大页是活的:

grep Huge /proc/meminfo mount | grep huge

HugePages_Free必须大于 0,/dev/hugepages必须挂载着。如果之前配了 1GB 页,这里会看到HugePages_Total: 8、Hugepagesize: 1048576 kB,说明 8 个 1GB 页就位了。

testpmd是最常用的自检工具,它能把网卡的收发包跑起来,不依赖任何业务逻辑,可以快速验证整条链路。启动参数里几个关键点:

dpdk-testpmd -l 2-5 -n 4 -a 0000:03:00.0 -a 0000:03:00.1 -- \ -i --nb-cores=2 --rxq=1 --txq=1

逐项解释:-l 2-5是绑定的逻辑核,注意避开核 0,核 0 通常跑着内核的中断和系统任务,让 DPDK 线程跑在上面会被打断。-n 4是内存通道数。-a是设备白名单(老版本是-w),一个-a对应一个设备。--之后是传给 testpmd 自己的参数,-i是交互模式,--nb-cores=2指定转发用的核数,--rxq/--txq是收发队列数,一般和核数对应。

进去之后:

testpmd> start testpmd> show port stats all

start之后两个口如果做了回环(用一根线把两个口直连),RX-packets和TX-packets应该快速增长,RX-errors为 0。如果包数一直是 0,先插拔一下线确认是千兆还是万兆的协商问题,再看show port info 0确认Link status是up。

-n这个参数在比较新的版本里已经废弃了,新版本会自动探测,写上去会给你一条警告但不影响运行。老教程里写的东西不一定对得上,看到警告别慌,看看提示说的是什么。

4.3 l2fwd 转发验证

testpmd验证的是收发包通路,l2fwd验证的是真正的转发逻辑:从一个口收进来,改一下 MAC 地址,从另一个口发出去。这个示例更适合拿来对照学习,代码量小、结构清晰。

dpdk-l2fwd -l 0-1 -n 4 -a 0000:03:00.0 -a 0000:03:00.1 -- -p 0x3

-p 0x3是端口掩码,0x3表示端口 0 和 1 都启用(二进制 11)。跑起来之后,两个口对接的外部流量应该能透传过去,终端上会周期性打印每个口的统计。

如果你想验证转发正确性,最好的办法是两边各挂一台机器,用ping加arp看 MAC 是否被改写,或者直接用一台机器发UDP流,另一台抓包看丢包率。这里就没tcpdump什么事了,因为口已经被 DPDK 接管,内核抓不到包,这也是前面说的“管理通道要另外规划”的具体体现。

l2fwd还有一个常被忽略的参数--no-mac-updating,加上它之后 MAC 不做改写,纯粹当个纯二层交换机用,调试的时候挺方便,可以排除 MAC 更新带来的干扰。

5. 排障实录:那些让人抓狂的报错

5.1 编译期问题速查

编译阶段的问题相对好解决,因为报错信息通常很直接。但有几个特别高频的,我整理成表,遇到直接对号入座。

报错信息原因解决办法
meson: command not found没装或版本太老pip3 install --upgrade meson
Run-time dependency libnuma found: NO缺numactl-devel装上后rm -rf build重新 setup
C compiler does not support C11编译器太老(CentOS 7 常见)用devtoolset切到高版本 GCC
ninja: error: loading 'build.ninja'在错误目录执行必须cd build再执行
fatal error: numa.h: No such file同 2,头文件缺失装numactl-devel
pahole: command not found缺dwarves包yum install dwarves
No such file: /lib/modules/xxx/build内核头没装或版本不匹配精确装kernel-devel-$(uname -r)

这里有一个特别隐蔽的坑:meson setup之后如果补装了某个依赖,直接重新ninja是不行的,因为 meson 把探测结果缓存在build/meson-logs/和内部数据结构里,必须删掉build目录重新 setup。很多人补装了numactl-devel之后发现还是同样的错,就是因为没清缓存。

另一个值得单独说的是 OOM。ninja并行编译的时候每个cc1进程都要吃几百 MB 到 1GB 内存,-j32在 16GB 的机器上很容易被 OOM killer 干掉,表现是编译到一半突然退出、没有任何错误信息、ninja报Killed。遇到这个先把-j调小。

5.2 运行期问题速查

运行期的报错才是真正的重灾区,因为 EAL(环境抽象层)的日志经常只给你一行结论,不告诉你原因。

报错信息根本原因处理方式
Cannot get hugepage information大页没配或没挂载检查/proc/meminfo和/dev/hugepages
No free hugepages reported大页被别的进程占满lsof /dev/hugepages找占用者
vfio: error, group N is not viableIOMMU 分组隔离不彻底开 ACS 或换插槽;退而用enable_unsafe_noiommu_mode
ethdev: no driver found网卡没绑或绑错驱动dpdk-devbind.py --status确认
EAL: Error - exiting with code: 1泛化错误,看前面几行日志往上翻,真正原因在前面
No probed ethernet devices-a参数写错检查 PCI 地址是否完整(含 domain)
failed to load session权限不足加--no-huge或提权

group is not viable这个问题值得多写两句。IOMMU 把设备按物理拓扑分成若干个组,同一个组内的设备没法单独隔离,DPDK 要求每个设备独占一个组。出现这个问题,先看ls /sys/kernel/iommu_groups/下的分组情况,如果发现网卡和别的设备挤在一起,最直接的办法是换个 PCIe 插槽,让它在物理上单独挂在一个根端口下。软件上的解法是打开 ACS,但这个功能需要主板芯片组支持,很多消费级主板并没有。

另外No probed ethernet devices这个报错,十有八九是 PCI 地址写得不完整。lspci显示的是03:00.0,但 DPDK 需要的是带 domain 的0000:03:00.0。这个细节小到不能再小,但确实能让人盯着屏幕发半天呆。

5.3 绑卡把自己 ssh 弄断的应急恢复

这个必须单独说,因为它发生的概率实在太高,而且一旦发生,机器就成了黑盒。

先说预防:绑卡之前ip link和lspci双向对照,确认哪个enpXsY对应哪个 PCI 地址,把要绑的地址写下来,管理口地址画个红圈。绑的时候一个一个来,绑完立刻确认 ssh 还活着再继续。用脚本批量绑定的,脚本里一定要把管理口排除掉。

真断了怎么办。如果有带外管理(IPMI、iDRAC 这类),直接进去操作;没有的话就只能物理接触。接入之后的第一件事是把网卡还给内核驱动:

# 查看当前所有设备的绑定状态 dpdk-devbind.py --status # 把设备还给原来的内核驱动 dpdk-devbind.py -b ixgbe 0000:03:00.0 # 或者解绑但不指定驱动,让它重新被内核探测 dpdk-devbind.py -u 0000:03:00.0

-u是解绑,解绑之后内核会自动重新识别并加载对应驱动,网卡就回来了。不知道原来是什么驱动的,看lspci -k的输出,Kernel driver in use那一行原本写的就是。

还有一个更彻底的后悔药:在绑卡之前记下原始状态。dpdk-devbind.py --status的输出完整存一份到/root/,出事了照着恢复,比凭记忆靠谱。

6. 性能调优中容易被忽略的细节

6.1 绑核与 NUMA 亲和

程序能跑起来和跑得好,中间隔着一整套 CPU 亲和性配置。最基本的一条:DPDK 用的核要和网卡在同一个 NUMA node 上。跨 NUMA 访问内存,延迟会翻倍,吞吐直接腰斩,而且这种损失在统计上看不出来,只有对比才知道。

先看拓扑:

lscpu | grep -i numa cat /sys/class/net/eth0/device/numa_node

网卡挂在哪一个 node,就挑那个 node 上的核来跑。-l参数给的是逻辑核编号,不是物理核编号,得先搞清楚映射关系:

lscpu -e

输出会列出每个逻辑核对应的 socket、core、node。挑核的时候,同一 socket 内的核之间共享 L3 缓存,跨 socket 的通信要走 QPI/UPI,差距很大。

再往深一层是超线程的问题。DPDK 轮询线程是 CPU 密集型,跑到两个共享物理核的超线程上会互相抢执行单元,性能反而不如单线程。我的习惯是关掉超线程,或者在-l里用lscpu -e挑出物理核编号,避开它们的兄弟线程。

6.2 内存通道与 C-state

-n 4这个参数在早年的教程里是必写的,指的是内存通道数。它影响的是内存控制器的交错访问模式,写错了会显著影响内存带宽。现在的新版本会自动探测,但如果你用的是老版本,一定要根据主板实际情况填——双通道填 2,四通道填 4。填错了不会报错,只是悄悄损失性能,属于最难发现的那类问题。

C-state 是另一个隐形杀手。CPU 进入深度睡眠状态再被唤醒,需要几十微秒,这个延迟对普通程序无感,但对微秒级的转发是致命的。虽然 DPDK 的轮询线程本身会让核保持在忙状态,但如果系统层面开了电源管理策略,其他核还是可能进 C-state,从而影响中断响应和内存访问的时序。生产环境的标准做法是在 BIOS 里关掉 C-state 和 P-state,或者在系统里设置性能模式:

tuned-adm profile latency-performance # 或者更激进的 tuned-adm profile network-latency

还有内核启动参数里的isolcpus,把要跑 DPDK 的核从内核调度器里摘出来,防止别的进程跑上去。比如isolcpus=2-9 nohz_full=2-9 rcu_nocbs=2-9,这三个参数配合使用,能显著减少这些核上的干扰。这一套配下来,机器上除了 DPDK 线程,别的什么都不干,这才是它的正常形态。

6.3 一些反直觉的经验

调试过程中有几条结论和直觉相反,但确实成立。

第一条:DPDK 程序跑起来之后 CPU 占用 100% 是正常的,不是卡死了。第一次用的人看到top里满屏 100% 会以为出了问题,其实那就是轮询。反过来,如果一个本该跑 DPDK 的核占用只有个位数,那才说明有毛病。

第二条:绑了网卡之后tcpdump抓不到包,不代表没有流量。内核根本看不见那个设备,抓不到是设计使然。想看包里有什么,得在 DPDK 程序里自己 dump,或者接一个镜像口。

第三条:大页预留多了会拖慢系统启动,预留少了会在启动时报错。这个数值不是越大越好,按实际需求算。我一般的算法是:每个 lcore 上跑一个 mempool,mempool 的大小按每包 2KB × 队列深度 × 若干倍余量估算,再乘上核数和端口数,最后留 20% 余量。比如 4 个核、2 个口、每核 8192 个缓冲、每个缓冲 2KB,大概是 4 × 2 × 8192 × 2KB ≈ 128MB,实际预留 1GB 就绰绰有余了。当然这取决于你程序里 mempool 的实际配置,只是个量级参考。

第四条:虚拟机里跑 DPDK 做验证可以,但别拿它的性能数据当结论。虚拟网卡的路径和物理网卡完全不同,跑出来的数字没有参考价值,只能用来验证代码逻辑正确性。

7. 最后说几句实操体会

装这一套东西真正费时间的从来不是编译那几分钟,而是前面的大页、IOMMU、绑卡这些环境配置,以及后面查询具体报错对应什么原因。我自己的习惯是准备一个配置清单,每次装新机器照着过一遍:内核版本对齐、依赖装全、大页配好、IOMMU 开、模块加载、留一个管理口、网卡地址记下来、绑卡后立刻验证 ssh 还在。这套流程走顺了之后,一台新机器从裸装到跑通testpmd大概半小时,第一次做的话一整天也很正常,别急,一步一步对。

有个小技巧值得分享:如果你只是在学习阶段,手头没有多余的物理网卡,可以用--no-huge配合--no-pci让程序跑起来,验证代码逻辑和 EAL 初始化流程,不碰真实网卡。虽然拿不到性能数据,但至少能把编译、链接、初始化这一整套走通,等有机器了再补真实环境的验证。这个方式对刚入门的人特别友好,避免在还没有任何概念的时候就去折腾 IOMMU 和硬件分组,那确实容易劝退。

另外,如果你后续要自己写基于 DPDK 的程序,建议从examples/里挑一个最接近的改,而不是从零开始写 EAL 初始化。那些rte_eal_init、rte_pktmbuf_pool_create、rte_eth_dev_configure的调用顺序和参数,官方示例已经把各种边界情况都处理过了,照着改既能少走弯路,也更容易看出每一步在干什么。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进