首页 / 资讯中心 / 文章详情

DPDK 从原理到实战:突破内核瓶颈的高性能数据包转发

DPDK 从原理到实战:突破内核瓶颈的高性能数据包转发 ★ FEATURED ARTICLE
1. 先从“为什么需要 DPDK”说起我做网络相关的开发有些年头了第一次接触 DPDK 是很早以前做流量分析项目的时候。那会儿我们处理单台机器的千万级数据包转发发现了一个非常尴尬的问题CPU 跑不满网卡也跑不满但包就是转不动。查来查去瓶颈根本不在于业务逻辑而是操作系统自带的网络协议栈把性能吃死了。要讲清 DPDK得先搞明白传统内核协议栈到底卡在哪里。正常情况下一个数据包从网卡进入到应用程序能处理走的路径是这样网卡收到包放进 DMA 环形缓冲区硬中断通知 CPUCPU 暂停手头的工作去处理中断把包从驱动里拷贝到内核协议栈的内存随后经历 IP 层、TCP/UDP 层的解析和重组最后调用 socket 接口把数据从内核空间拷贝到用户空间整个流程才算走完。如果包量大还有软中断、锁竞争、上下文切换每一步都在吃掉宝贵的 CPU 周期。DPDK 的出现说白了就是换了一种思路既然内核协议栈是瓶颈那我就不走内核既然中断通知机制开销大那我就改成轮询既然内核态和用户态之间拷贝数据耗时那我就让用户态程序直接操作网卡内存。一句话概括DPDK 是一套基于用户态轮询驱动的数据平面开发套件它把数据包的收发和控制从内核协议栈中剥离出来交给用户态程序直接处理从而实现极高的包处理吞吐量。这篇文章我会围绕几个核心问题展开DPDK 为什么快它的核心机制到底怎么运转实际部署要怎么做哪些场景适合用它、哪些场景根本不该用以及新手入坑最常踩的坑有哪些。不管你是在做防火墙、负载均衡、流量分析还是边缘网关只要涉及高频数据包处理这篇文章都能给你一个相对完整的参考。2. DPDK 的性能密码它到底做了什么很多第一次接触 DPDK 的人会有一个误解觉得 DPDK 只是一套驱动库把网卡驱动换成 DPDK 版本就完事了。实际上 DPDK 是一整套用户态包处理框架它围绕一个核心目标展开最大程度减少数据包处理路径上的每一笔开销。要理解它的设计逻辑得从几个关键机制逐一拆开看。2.1 用户态驱动把网卡的控制权拿到手传统网卡驱动的逻辑是一个内核模块网卡中断触发后内核运行驱动代码去 DMA 环形缓冲区取包然后往上送。DPDK 的思路完全不同它把网卡驱动的核心逻辑直接搬到了用户态发送和接收队列的环形缓冲区由用户态程序直接管理。用户态驱动通过 UIO 或 VFIO 机制把设备文件和 DMA 内存映射到用户空间这样应用程序就能直接读写网卡的硬件队列。举个例子你用 DPDK 的rte_eth_rx_burst函数收包本质上就是从一块用户态就能直接访问的内存区域里取出描述符然后拿到指向数据包的指针。全程没有系统调用没有内核参与没有数据拷贝。这正是 DPDK 能实现高性能的根本原因之一。2.2 大页内存与无拷贝设计传统内存分页默认是 4KB数据包频繁收发会导致 TLB 缓存频繁失效。TLB 是 CPU 内部用于加速虚拟地址到物理地址翻译的缓存一旦失效CPU 就得去查页表开销不小。DPDK 默认使用 2MB 甚至 1GB 的大页内存让同样的内存区域对应更少的页表项TLB 命中率明显提升。同时DPDK 的 mbuf内存缓冲区结构直接在大页内存里分配网卡 DMA 把数据包写入这个位置之后用户态程序拿到的就是真实的数据位置直接解析、修改、转发不需要再复制一份。你想想一个 64 字节的小包如果每次收发包都要做两次内存拷贝1000 万包每秒的流量下光是拷贝就吃掉大量带宽和 CPU无拷贝设计省掉的开销非常可观。2.3 轮询模式取代中断模式中断模式的优点是 CPU 在空闲时不被打扰但代价是每次有包到达都要打断 CPU打断之后还有上下文切换、cache 失效的恢复成本。在满负载场景下中断风暴会让 CPU 疲于奔命大量时间花在中断处理而不是包处理上。DPDK 的 PMDPoll Mode Driver驱动把中断换成了轮询CPU 会在一个循环里不断检查接收队列里是否有新的描述符。有人看到轮询会觉得浪费 CPU实际上在持续有流量的场景下轮询比中断高效得多因为 CPU 始终在忙包处理没有上下文切换的开销。数据中心和核心网设备几乎不存在空闲时段所以这个取舍是合理的。2.4 CPU 亲和性与无锁化DPDK 应用通常会把处理线程绑定到固定 CPU 核心上避免线程在不同核心间迁移导致 cache 命中率下降。同时DPDK 提供无锁队列ring buffer用于核心之间的通信。一个核收包另一个核转发两个核之间通过 ring buffer 传递数据全程无锁。无锁队列的原理是使用读指针和写指针配合原子操作避免多个线程同时修改同一个数据结构引发的竞争。配合每个核独占一个队列的模式DPDK 在多核环境下依然能保持接近线性的性能扩展。这也是为什么很多 DPDK 应用会输出一份 lcore 分配图明确哪个核负责收包、哪个核负责计算、哪个核负责发包。3. 实际部署从零搭一个 DPDK 环境讲理论容易真正动手的时候不少人会在环境准备阶段卡住。DPDK 的开发环境其实不复杂但对系统有一些特殊要求下面我按照完整的流程走一遍顺便把容易踩坑的地方标出来。3.1 环境准备与前置条件你需要一台 Linux 机器内核版本建议 3.10 以上推荐较新的发行版。CPU 必须支持或开启 IOMMU如果要用 VFIO 的话BIOS 里通常叫 VT-d 或 AMD IOMMU。内存方面建议准备至少 2GB 的预留大页空间用于测试。安装 DPDK 之前确认系统有这几样东西内核头文件包编译内核模块时需要GCC、make、meson、ninja 等编译工具pciutils用来查看 PCI 设备信息Python 3部分构建脚本依赖如果你用的是较新的 DPDK 版本20.11 之后默认构建系统已经换成了 meson不再使用早期的 make 方式。我自己现在习惯直接用 meson 编译效率高很多$ tar xf dpdk-22.11.tar.xz $ cd dpdk-22.11 $ meson setup build $ ninja -C build如果之前装过旧版 DPDK建议先重新编译一次避免新旧库混用导致运行时报符号找不到这种诡异问题。编译完成后把库路径写进环境变量$ export RTE_SDK/path/to/dpdk $ export RTE_TARGETx86_64-native-linux-gcc3.2 大页内存配置方法大页内存是 DPDK 运行的基本条件配置不对程序直接起不来。临时配置大页可以用下面的命令$ echo 1024 /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages这条命令的意思是预留 1024 个 2MB 大页总共 2GB。要永久生效的话可以修改/etc/sysctl.conf加上vm.nr_hugepages 1024然后sysctl -p让它生效。另外还需要把大页内存挂载到文件系统挂载点$ mkdir -p /mnt/huge $ mount -t hugetlbfs pagesize2MB /mnt/huge为了省事可以写进/etc/fstab否则重启机器后又得手动挂载。我在实际项目中吃过这个亏机器一重启DPDK 应用直接报EAL: No free hugepages reported in hugepages-2048kB排查了半天才发现是大页没挂载。3.3 网卡绑定与解绑操作DPDK 应用需要使用指定的网卡端口默认情况下网卡驱动是内核模块比如 ixgbe、i40e需要把它解绑改绑到 DPDK 支持的 igb_uio 或 vfio-pci 驱动上。以太网设备改绑的示例命令如下$ modprobe vfio-pci $ dpdk-devbind.py --bindvfio-pci 0000:03:00.00000:03:00.0是网卡的 PCI 地址可以用dpdk-devbind.py -s查看当前所有网络设备的 PCI 地址和绑定状态。选择 vfio-pci 还是 igb_uio取决于你的平台是否支持 IOMMU。我建议优先用 vfio-pci它支持更完整的安全隔离特性igb_uio 是早期方案功能上够用但平台兼容性略差。注意解绑之前确认这块网卡没有配置 IP 地址、没有承载业务流量否则解绑瞬间网络就断了远程操作的话会直接掉线。3.4 最小可运行示例l2fwd 实测环境准备好之后可以用 DPDK 自带的示例程序验证一下整体链路是否走通。l2fwd 是最简单的二层转发示例把收到的包从另一个端口发出去非常适合做环境验证。先设置大页环境变量$ export DPDK_OPTIONS-l 0-3 -d 0:0.0 --huge-dir/mnt/huge其中-l 0-3指定使用 0 到 3 号四个逻辑核心-d或-a指定 PCI 设备地址。然后运行$ ./build/examples/dpdk-l2fwd -l 0-3 -a 0000:03:00.0 -a 0000:03:00.1 -- -p 0x3 --no-mac-updating参数-p 0x3表示启用端口掩码为二进制的 11意思是使用两个端口。--no-mac-updating表示不修改 MAC 地址适合纯转发测试。如果一切正常你会看到终端里有类似转发线程运行的日志输出说明你的 DPDK 环境已经可以使用了。第一次跑的时候遇到一个问题程序提示EAL: Detected lcore 0 as core 0 on socket 0然后就卡住了后来发现是--huge-dir参数指定的挂载点不对大页挂载在/mnt/huge而我传了/dev/hugepages指向错误导致内存分配一直等待。检查配置之后重新运行就正常了。4. DPDK 核心 API 与编程模型环境通了之后接下来就是写代码。DPDK 的应用编程模型和传统 socket 编程差别很大第一次写可能需要一点时间适应。下面我把最核心的几个 API 和编程套路串一遍。4.1 EAL 初始化与 lcore 分配任何 DPDK 应用的第一步都是调用rte_eal_init它会初始化大页内存、PCI 设备、日志系统、lcore 配置等底层资源int ret rte_eal_init(argc, argv); if (ret 0) { rte_exit(EXIT_FAILURE, Error with EAL initialization\n); }这个函数会把你在命令行传入的参数比如-l 0-3、-a 0000:03:00.0解析到全局配置中。初始化完成之后可以通过rte_lcore_id()获取当前线程所在的 lcore ID通过RTE_LCORE_FOREACH_WORKER(lcore_id)遍历所有可用的 worker lcore。我实际写多核转发程序时通常会采用主从模式主线程负责初始化、周期性统计、处理控制面消息从线程绑定到不同的 worker lcore每个 worker 各自循环处理收包、转发逻辑。4.2 内存池与 mbuf 操作rte_pktmbuf_pool_create用于创建内存池之后所有的数据包 mbuf 都从该池中分配。创建时指定的cache_size和socket_id需要认真考虑其中socket_id决定内存从哪个 NUMA 节点分配最好与绑定网卡的 CPU 控制器在同一条路径上否则跨 NUMA 访问会有延迟。struct rte_mempool *mbuf_pool rte_pktmbuf_pool_create(MBUF_POOL, NUM_MBUFS, 512, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());收包时调用rte_eth_rx_burst从网卡接收队列中拿一批 mbufrte_pktmbuf_mtod拿到数据指针长度从mbuf-pkt_len获取。发包时调用rte_eth_tx_burst把整理好的 mbuf 数组交给网卡发送。这里容易犯的一个错误是忘记释放不再使用的 mbuf。内存池的大小是有限的如果只收包不释放池耗尽之后收包接口会一直返回 0而且不会报任何错误排查起来非常隐蔽。我一般会在处理完每个包之后调用rte_pktmbuf_free或者用rte_pktmbuf_bulk_free批量释放。4.3 收包转发主循环的完整写法以二层转发为例一个最简单的主循环是这样for (;;) { nb_rx rte_eth_rx_burst(port, queue_id, rx_pkts, BURST_SIZE); if (nb_rx 0) { continue; } for (i 0; i nb_rx; i) { // 处理逻辑修改 MAC 地址、查转发表、VLAN 处理等 out_port get_dst_port(rx_pkts[i]); tx_pkts[out_port][tx_count[out_port]] rx_pkts[i]; } // 批量发送提高效率 for (i 0; i MAX_OUT_PORTS; i) { if (tx_count[i] 0) { rte_eth_tx_burst(i, queue_id, tx_pkts[i], tx_count[i]); tx_count[i] 0; } } }有几个细节值得注意。一是BURST_SIZE的选择常用的值有 32、64、128、256 等。我用下来感觉 64 是平衡性比较好的选择太大会增加延迟和缓存压力太小则摊不薄函数调用开销。二是收包后一定要预留一个发包前的组装步骤不要一个包收进来就立刻发出去那样效率很低因为每次rte_eth_tx_burst调用都有固定成本攒一批再发明显更优。4.4 多队列与 RSS 分流策略多核容易但要让每个核的工作量均衡就要靠网卡的多队列和 RSSReceive Side Scaling机制。RSS 的作用是根据包的五元组信息做哈希然后把不同的流分发到不同的接收队列每个队列绑定一个单独的 CPU 核心处理这样同一连接的包会落在同一个核心上避免乱序和锁竞争。DPDK 中配置 RSS 需要在端口初始化时设置rte_eth_conf里的mq_mode为RTE_ETH_MQ_RX_RSS并设置rte_eth_rss_conf中的哈希类型。配置完每队列的 ring 大小也要留意默认值是 512如果流量比较猛可以调到 2048但注意 ring 越大占用的内存也越多。RSS 虽好也不是万能的。如果流量模型是少数大流量连接哈希分流的均匀性会很差这时候需要自定义报文分发逻辑或者使用 DPDK 的 Flow Filter 功能按特定字段做定向分发。我遇到过一个现场四队列配置下有一个队列跑到 80% 利用率另外三个队列不到 20%就是典型的哈希碰撞严重后来加了扩展哈希字段才均衡了。5. DPDK 适用的场景与不适用的场景很多人容易走极端要么认为 DPDK 是万能的性能银弹要么觉得它太复杂完全没必要学。实际上 DPDK 的适用范围挺明确用对了很香用错了就是折腾自己。5.1 高流量转发场景是主战场DPDK 最典型的应用场景是高频数据包处理比如流量分析、IDC 防火墙、负载均衡器、DPI 设备、接入网关、5G UPF 用户面功能等。这些场景的共性需求是单机要扛住百万甚至千万级 PPS每秒包数数据包的处理逻辑相对简单直接不需要和操作系统上层生态做太多交互。举个实际例子。我参与过的流量采集项目中原来的方案是基于内核 AF_PACKET 抓包单机到大约 20 万 PPS 时 CPU 就已经一个核跑满了丢包率开始上升。后来把数据面迁到 DPDK同样一台机器轻松达到 200 万 PPSCPU 占用率不到一半。这种数量级的提升不是靠优化代码能实现的只有从架构层面绕过瓶颈才能做到。5.2 复杂协议处理与多业务交互不建议用 DPDKDPDK 不适合的场景也有不少。如果你的业务依赖 TCP 连接管理系统、需要调用 Linux 网络栈的现成协议功能比如监听端口、主动发起 TCP 连接、使用 Linux 自带的路由/隧道/VXLAN 能力硬上 DPDK 等于把大量工作揽到自己身上你需要自己实现 TCP 栈、自己维护连接表、自己处理 ICMP 报文工程复杂度会呈指数级上升。另外低频控制面、管理面操作也别用 DPDK。比如管理后台需要定期和外部系统通信、走 HTTPS 调 API这种情况就应该保留一个管理网口给内核协议栈用DPDK 只负责数据面转发。两者各司其职才能发挥各自优势。我看到不少团队踩过这种坑一个功能其实内核协议栈就能满足需求但团队为了“性能更好”强行引入 DPDK结果开发周期从两周拖到两个月最后还引入了一堆稳定性问题。性能提升是有边际效应的先明确业务约束再选型才靠谱。5.3 DPDK 与云原生的权衡容器化环境里面做 DPDK 需要额外注意。DPDK 使用大页内存、绑定网卡、设置 CPU 亲和性这些都和容器默认的隔离机制存在冲突风险。在 Kubernetes 里跑 DPDK 应用时需要用到 SR-IOV、CPU 管理器、HugePage 资源这些高级特性实际上是完全可行的但复杂度比裸机直接部署高出不少。如果你只是想在容器里开发调试 DPDK 程序可以用纯软件的方式跑比如 DPDK 的pcap或af_packetPMD。这两个驱动不需要物理网卡一个使用 pcap 文件一个使用内核 AF_PACKET 接口性能虽然比不上物理网卡直通但拿来学习 API、跑通逻辑流程完全够用。我第一次写 DPDK 转发程序就是用的这种模式在普通服务器上调试跑通之后再上真实网络环境省了不少事。6. 性能调优与实战排坑环境搭好、程序能跑这只是第一步。真正到了生产环境你会发现很多事情不按常理出牌。这一节我把性能调优的常用手段和实战中遇到的典型问题整理一下。6.1 NUMA 架构下的内存和核心分配现代服务器基本都是多路 CPU 加 NUMA 架构每个 CPU 控制器管理一部分内存跨 CPU 访问内存会有额外延迟。DPDK 性能调优的第一原则就是网卡插入哪个 NUMA 节点就用哪个节点上的 CPU 核来处理这张网卡的流量。查看网卡所属 NUMA 节点可以用lspci -v看到类似NUMA node: 0这样的信息就是节点编号。运行时可以通过rte_eth_dev_socket_id(port_id)在代码里获取然后和rte_lcore_to_socket_id(lcore_id)返回值对比保证一致。如果跨 NUMA 了内存池分配也需要显式指定socket_id否则默认分配在当前执行线程所在的节点上。这一项做不好性能掉个 20% 很正常。我印象很深的一次调优就是只把核心挪到了网卡同节点转发吞吐就从 80 万涨到了 110 万 PPS。6.2 收包性能优化Burst 与预取的组合拳前面提到收包使用 burst 模式一次拿多个包这一步还能进一步优化。每拿一批包后可以先用rte_prefetch0预取下一批包的数据到 CPU 缓存利用内存访问的流水线特性把耗时的 cache miss 掩盖掉。收到 64 个包可以先对前 32 个包手动预取数据再逐个处理这 32 个处理完后预取后 32 个这样 CPU 等待内存的时间能明显减少。示例代码如下for (i 0; i nb_rx; i) { rte_prefetch0(rte_pktmbuf_mtod(rx_pkts[i 1], void *)); process_packet(rx_pkts[i]); }注意不要预取超出实际接收数量的位置否则会读到无效地址导致段错误。这个优化对每个包处理逻辑较重的场景尤其明显纯转发场景提升相对有限但总归没有坏处。6.3 常见报错速查表我在不同机器上部署 DPDK 的过程中遇到过不少报错整理成一张速查表方便你遇到类似问题时快速定位。报错信息常见原因处理办法EAL: No free hugepages reported大页内存未预留或未挂载检查/sys/kernel/mm/hugepages配的 nr_hugepages确认挂载了 hugetlbfsEAL: unsupported IOMMU type内核未开启 IOMMU 或 VFIO 支持内核启动参数加intel_iommuon确认CONFIG_VFIO已编译进内核EAL: cannot open VFIO container当前用户无权限操作 /dev/vfio添加用户到 vfio 组或者用 root 运行ethdev: failed to configure RSS网卡不支持所选 RSS 配置检查网卡 datasheet 支持的 hash 类型降级配置rte_eth_rx_burst: not ready网卡未正确启动或队列配置异常检查代码中是否调用了rte_eth_dev_start确认队列数量和大小配置合理EAL: PCI device ... is not managed by any kernel driver网卡未绑定到 DPDK 驱动执行dpdk-devbind.py --bindvfio-pci 设备地址程序启动后立即段错误内存池分配失败或静态变量被多线程同时写加RTE_SET_USED排除警告检查每个 lcore 使用的变量是否独立这张表只是常见问题的一部分。真实排查时我会习惯在程序启动时先开启 debug 日志命令加--log-level8它会打印更多初始化细节很多问题在日志里其实已经说明得很直白了只是默认级别被隐藏了。6.4 从 10 万到百万 PPS 的调优路线图很多新手用户按照默认配置能跑几个万包每秒就以为 DPDK 不过如此。其实从默认状态到高性能状态是有明确调优路线的我建议按这个顺序逐步推进第一步确认大页内存用的是 2MB 还是 1GB条件允许的话最好用 1GB 大页TLB 命中率更高。第二步检查 CPU 频率是否为性能模式有些系统默认是节能模式CPU 降频之后性能直接打折。第三步绑定 CPU 核并确认和网卡所属 NUMA 节点一致。第四步把收发包的 burst 大小调到 64 或 128加 prefetch。第五步开启网卡硬件卸载功能比如 checksum offload、TSO/GRO减轻 CPU 负担。第六步如果还嫌不够可以开启 DPDK 的rte_eth_rx_offload里面的 RSS 多队列并用rte_flow规则做更细粒度的分流。这份路线图每走一步都会带来可感知的性能提升。我见过很多项目里仅仅是把大页从 2MB 换成 1GB转发性能就有 10% 以上的提升这还只是一行配置的改动。7. 我的一些心得体会做 DPDK 相关的开发这几年踩过不少坑也积累了一些判断经验。分享几个我觉得挺重要的点。第一学习 DPDK 不要一上来就想做复杂的转发应用。先把示例代码跑通改改参数看看性能变化然后把rte_ring、rte_mempool这些基础组件单独拆出来写几个小实验理解每个组件的行为特征。基础扎实了再上复杂功能调试起来会顺手很多。第二DPDK 项目调试最好复用 DPDK 自带的dpdk-testpmd工具。它可以直接发包、收包、统计吞吐拿来排查网卡硬件和驱动问题非常合适。如果 testpmd 都达不到预期性能大概率是硬件或环境问题而不是你的应用代码问题。第三任何 DPDK 应用上线前都要跑一轮长时间稳定性测试。轮询模式意味着 CPU 永远处于 100% 满载状态散热、电源、CPU 降频都会影响稳定性。我们这边实际部署时专门做过 72 小时连续运行验证内存泄漏问题往往在这个阶段才会浮出水面。第四遇到性能问题别急着怀疑 DPDK 框架本身。更多时候问题出在应用侧核心分配不均、内存跨 NUMA、过度临时的内存申请、没有批量收发这些才是真正拖后腿的地方。拿perf top看一眼热点分布往往比瞎猜原因更有效。DPDK 是一个工具不是目标。搞清楚它的原理知道它能解决什么问题、不能解决什么问题在合适的场景里把它用好这比单纯追求高深的技术名词有价值得多。
阅读完成 · 觉得有帮助?
咨询建站