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

深入浅出DPDK:用户态驱动、大页内存与无锁队列实战指南

深入浅出DPDK:用户态驱动、大页内存与无锁队列实战指南 ★ FEATURED ARTICLE
简介《深入浅出DPDK》全书读书笔记是一份面向网络开发工程师、DPDK初学者及虚拟化/NFV从业者的技术整理系统梳理了高性能网络I/O框架的关键知识。整份内容浓缩为单个PDF文件6.57MB目前已有3849人学习。笔记从传统网卡中断驱动讲到DPDK用户态轮询覆盖多队列流分类、NAPI机制、Netmap共享包池、用户态驱动及降低访存开销等核心优化思路还展开介绍了DPDK核心库、rte_eal_init初始化流程、精确匹配/LPM/ACL分类库、SR-IOV与virtio接口以及NFV/SDN中的数据面软化趋势。阅读后可对DPDK的架构设计、内存管理、包处理路径和虚拟化方案形成系统认识适合作为案头参考或备考复习提纲。1. 从一枚网卡中断说起为什么非要读 DPDK做网络后台开发的人迟早会撞上 DPDK 这个词。无论你是做网关、做负载均衡、做防火墙还是做 CDN 边缘节点只要流量一大「内核协议栈扛不住」这句话就会反复出现。DPDK 的全称是 Data Plane Development Kit它解决的核心问题非常直接让用户态程序绕过内核直接读写网卡数据。这套思路在中文技术圈里讲了十几年但真正能把原理、代码、参数串成一条线讲清楚的中文资料《深入浅出DPDK》这本书算是绕不开的一本。而这篇笔记就是把原书里最值得用的部分拆成可落地的操作路径——不替你把书读了而是告诉你从哪读、读完怎么动手、动了手会遇到哪些坑。我当年第一次跑通 DPDK 的 l2fwd 示例程序时心里就一个念头原来网卡还能这么玩。这篇文章适合两类人一类是被「高并发网络」逼着上 DPDK 的后台开发另一类是刚接触数据面开发、想搞清楚 DPDK 安装与收包链路的新手。下面按我自己的阅读和落地顺序把这套东西讲透。2. 读懂 DPDK 的三大基石用户态驱动、大页内存、无锁队列2.1 用户态驱动PMD是 DPDK 一切性能的起点《深入浅出DPDK》这本书的安排很有意思它不是一上来就教你怎么用 API而是先把网卡收包的完整路径画出来。传统方式下网卡收到数据包通过中断通知内核内核把报文从驱动拷贝到协议栈再经过 socket 交给应用。这中间有上下文切换、数据拷贝、软中断处理每一层都在消耗 CPU。DPDK 的做法是把网卡驱动搬到用户态这部分在书里叫 PMDPoll Mode Driver。PMD 意味着网卡收到包后不主动触发中断而是用户态程序持续轮询网卡的接收描述符。代价是 CPU 会一直转收益是包到达应用路径上的延迟和抖动大幅下降。这本书里对 PMD 的描述有一句话让我印象很深轮询不是一种技巧而是一种取舍。它用固定的 CPU 占用换最确定的收包延迟。你如果只记住一个概念就记住这个。网上很多人说 DPDK 是「用户态网卡驱动」严格说并不完整它还包括内存管理、无锁队列、定时器、报文转发框架但驱动层确实是一切的前提。没有 PMD后面的任何优化都无从谈起。2.2 大页内存为什么普通 malloc 在 DPDK 里不够用读这本书第二个要建立的概念是大页内存Hugepages。CPU 访问内存走 TLBTLB 能缓存的页表项有限。常规页面是 4KB如果程序需要访问 1GB 的报文缓冲区就需要 262144 个页表项TLB 根本装不下结果就是频繁缺页每次访问内存都伴随页表遍历。DPDK 把这块内存池固定在 2MB 或 1GB 的大页上TLB 命中率大幅提升报文收发路径上的内存访问开销也就降了下来。在看书的过程中你会发现大页内存并不是 DPDK 独有的需求任何高性能中间件都可能使用它。但 DPDK 对它的依赖是硬性的没有配置大页内存DPDK 程序根本无法初始化。书里给了一个很实用的数字建议单核收包场景下至少预留 128MB 大页内存如果你要跑 8 个转发核建议按 1GB 起步。这个数字不是拍脑袋定的它跟默认内存池大小、收发描述符数量、以及每个报文保留的头部空间有关。2.3 rte_ring理解无锁队列的边界才能用好它《深入浅出DPDK》里最有营养的部分我以为是 rte_ring 那一章。它是 DPDK 的无锁环形队列生产者和消费者之间通过读写下标来通信不依赖互斥锁。多生产者多消费者模式下它的核心手段是 CASCompare And Swap这和内核里常见的 spinlock 有本质区别——spinlock 让等待者自旋CAS 让写入者重试。写入竞争不激烈时rte_ring 的吞吐可以做到接近内存拷贝的上限但竞争激烈时CAS 重试带来的 cache miss 也很惊人。书里有一个数值得背下来rte_ring 的容量必须声明为 2 的幂次方。原因是它的空闲槽位计算依赖位运算只有容量是 2 的幂才能用 (prod_tail - cons_tail) (size - 1) 拿到准确的已用空间。很多人第一次写 rte_ring 时忘了这个约束程序跑起来半秒钟就崩或者丢包率异常高就是这里踩的坑。绕开这个限制的唯一正规做法是设置 RING_F_SC_DEQ 等标志但带标志也绕不开 2 的幂次方这个底层约束。提示读这本书的第二章到第四章时建议手边放一份 DPDK 源码。rte_ring.h 里的注释比很多博客讲得清楚尤其是内存屏障的使用位置源码里标得一清二楚。3. 把笔记变成环境DPDK 安装与两个最小示例的完整复现3.1 从源码到可运行dpdk 安装与编译的五个关键步骤读书笔记里最容易被跳过的部分是环境搭建但它恰恰是最容易劝退新手的地方。常见做法是直接从 DPDK 官方源码编译版本号建议选 LTS 版本比如你用的是 20.11 或 21.11而不是最新的实验版本。实验版本代码更新但依赖的编译器版本和内核头文件常常对不上编译到一半报错时搜不到对应的解决方案非常折磨人。下面是安装的完整命令# 下载源码并解压 wget https://fast.dpdk.org/rel/dpdk-21.11.5.tar.xz tar -xf dpdk-21.11.5.tar.xz cd dpdk-21.11.5 # 加载大页内存单页 1GB共 4 个页面 mkdir -p /mnt/huge mount -t hugetlbfs pagesize1GB nodev /mnt/huge echo 4 /sys/devices/system/node/node0/hugepages/hugepages-1048574kB/nr_hugepages # 编译 DPDK 库 meson setup build ninja -C build ninja -C build install ldconfig理解这段命令关键在中间两个环节。mount 这一步是让大页内存有挂载点echo 这一步是真正向系统申请大页。为什么用 1GB 而不是 2MB因为 1GB 大页的 TLB 覆盖范围更大但代价是分配粒度太粗如果机器内存吃紧建议退回 2MB。用grep Huge /proc/meminfo能确认是否分配成功看到HugePages_Total: 4才说明这一步生效了。meson 和 ninja 是 DPDK 当前版本的标准构建工具链早期版本用的 make 已经被淘汰了——如果你在网上搜到老帖子里写 make config直接略过版本对不上。编译装上之后还要处理运行环境变量。pkg-config --cflags --libs libdpdk可以输出编译客户端程序所需的头文件路径和链接库参数。这一步我建议做成一个 shell 变量后面写 DPDK 程序编译命令时直接用省得反复记路径。3.2 跑通第一个示例helloworld 验证环境环境装好后的第一件事不是直接上 l2fwd而是先跑一个最简单的 helloworld。DPDK 源码自带这个示例路径在 examples/helloworld。它做的事情就是把每个 CPU 核心的编号打印出来目的是验证 EALEnvironment Abstraction Layer能不能正常初始化。EAL 是 DPDK 的底层抽象层负责大页内存映射、CPU 亲和性、PCIe 设备探测——你可以把它理解成 DPDK 的「操作系统」。# 编译 helloworld 示例 cd examples/helloworld gcc -o helloworld main.c $(pkg-config --cflags --libs libdpdk) # 用 4 个内存通道、2 个核心运行 ./helloworld -l 0-1 -n 4运行后如果看到类似lcore 0 ready和lcore 1 ready的输出说明 EAL 初始化成功大页内存也生效了。其中-l 0-1指定使用的逻辑核范围-n 4指定内存通道数这个参数必须和主板的实际内存通道数匹配这个数字你可以通过dmidecode -t memory查也可以直接试 4绝大多数 Intel 平台默认就是 4 通道。如果你的机器上跑 helloworld 直接崩了报的错误是找不到可用的大页内存或无法映射 PCI 资源那基本可以确认是前面第 3.1 节的挂载或/sys配置出了问题先回去检查/proc/meminfo。3.3 l2fwd第一个真正在转发报文的 DPDK 程序helloworld 只是证明环境能跑真正让笔记落到实处的启动示例是 l2fwd。它的功能是把网卡收到的数据包原样从另一个端口发出去属于最简单的二层转发模型。我建议每个读这本书的人都把 l2fwd 亲手跑通一次因为它涉及了收包、发包、内存池、队列绑定和转发循环的完整链条。# 绑定网卡到 DPDK 用户态驱动 dpdk-devbind.py --status # 查看当前网卡状态 dpdk-devbind.py --bindvfio-pci 0000:02:00.0 0000:02:00.1 # 编译并运行 l2fwd cd examples/l2fwd gcc -o l2fwd main.c $(pkg-config --cflags --libs libdpdk) ./l2fwd -l 0-1 -n 4 -- -p 0x3 --txq1 --rxq1--bindvfio-pci这步是把物理网卡从内核驱动比如 igb、ixgbe摘下来交给 DPDK 的 PMD 接管。摘之前必须确认这张网卡上没有在跑的业务否则网络瞬间断开。-p 0x3是端口掩码二进制是 11表示使用两个端口--txq1和--rxq1是每个端口的收发队列数测试环境一个队列就够生产环境再根据 CPU 核数做多队列映射。跑起来之后用iperf3从对端机器打流你会看到 DPDK 转发的吞吐数据。如果 l2fwd 跑起来后没有任何收包计数先别怀疑网卡坏了大概率是--bind没有生效用dpdk-devbind.py --status再次确认网卡状态是否变成drvvfio-pci同时确认端口掩码没有写错。注意一台物理机上同时只有一张网卡时-p 0x3会提示找不到第二个端口。测试 l2fwd 最少需要两张网卡或者用一个支持 SR-IOV 的网卡创建两个 VF很多人在这个地方卡半天。3.4 从笔记到工程看懂 l2fwd 主循环l2fwd 示例的源码并不长但它的主循环结构是后续所有 DPDK 应用的原型值得逐行拆解。核心逻辑在l2fwd_main_loop()函数里里面做了四件事从网卡收包、检查包的类型、构造或查找交换信息、把包发送到目标端口。这里最重要的两个 API 是rte_eth_rx_burst和rte_eth_tx_burst它们的名字里带 burst突发意味着一次调用处理一批包而不是一个包。// l2fwd 主循环核心片段节选 for (;;) { // 从端口 portid 的队列收包一次最多收 nb_pkt 个 uint16_t nb_rx rte_eth_rx_burst(portid, queueid, pkts_burst, MAX_PKT_BURST); if (unlikely(nb_rx 0)) continue; // 遍历收进来的每个包根据目标 MAC 决定从哪个端口发出 for (i 0; i nb_rx; i) { struct rte_mbuf *m pkts_burst[i]; rte_prefetch0(rte_pktmbuf_mtod(m, void *)); // 这里按实际业务处理报文示例里直接转发 } // 把处理后的包批量发送出去 uint16_t nb_tx rte_eth_tx_burst(dst_port, queueid, pkts_burst, nb_rx); }rte_eth_rx_burst的返回值是实际收到的包数量rte_prefetch0是 DPDK 常用的预取指令它把包的数据提前加载到 CPU 缓存这样后续处理时缓存命中率更高这个操作在转发场景下能带来 10% 以上的性能提升。这段代码里的端口和队列概念也值得反复琢磨每个物理端口可以配置多个收发队列每个队列运行在不同的 CPU 核上这就是 DPDK 多核扩展的基础。4. 深入笔记核心rte_mempool、rte_mbuf 与端口队列的参数设计4.1 rte_mempool 的参数为什么直接决定吞吐上限《深入浅出DPDK》书里讲内存管理的篇幅不少而 rte_mempool 是 DPDK 内存管理的核心。它本质上是一个预先分配好的对象池所有报文缓冲区都从这里取。rte_mempool 有两个参数需要你反复调整缓存大小cache_size和元素大小elt_size。cache_size 是指每个 CPU 核的本地缓存块数量这个值太小会导致频繁访问全局池产生锁竞争太大会浪费内存而且也会降低缓存的周转效率。常见的做法是设置为 256 或 512如果你机器的 L2 缓存较大可以试着提到 1024 做对比测试。// 创建一个报文内存池nb_mbufs8192, cache_size256 struct rte_mempool *mbuf_pool rte_pktmbuf_pool_create( mbuf_pool, 8192, // 池中缓冲区个数 256, // 每核本地缓存数 0, // 私有数据大小 RTE_MBUF_DEFAULT_BUF_SIZE, // 每个缓冲区的数据区大小 SOCKET_ID_ANY );elt_size对应的就是上面的RTE_MBUF_DEFAULT_BUF_SIZE默认值是 2176 字节其中 2048 字节是数据区剩余是 mbuf 头部和头部预留空间。生产环境里如果报文主要是 1500 字节的 MTU 大小这个默认值是合理的但如果你的业务有大量巨型帧jumbo frame就必须把这个值调大否则收包时直接报 buffer too small 的错误。还有一个容易被忽略的参数是socket_id多 NUMA 节点机器上内存池必须按节点分别创建否则跨 NUMA 访问内存带来的延迟会直接毁掉 DPDK 的性能优势。判断方法很简单收包网卡在哪个 NUMA 节点内存池就建在哪个节点用rte_eth_dev_socket_id(portid)查询。4.2 rte_mbuf别忽略头部空间和分片rte_mbuf 是 DPDK 里的报文描述符它的结构设计是本书里另一个值得精读的点。一个 mbuf 不止保存报文数据还保存了元数据端口号、时间戳、报文长度、下一跳信息等。初次接触 DPDK 的人最常犯的错误是直接从rte_pktmbuf_mtod拿到的地址往前偏移自己拼接自定义头部结果覆盖了 mbuf 结构体本身。正确做法是用rte_pktmbuf_prepend和rte_pktmbuf_append来操作头部和数据区。prepend是在现有数据前面加一段空间用于封装新协议头append是在数据末尾追加空间。这两个函数内部会检查余量不会越界写。mbuf 还有一个重要字段是pkt_len它表示整个报文的总长度data_len表示当前段的数据长度。做转发的时候修改了报文内容一定要同步更新这两个字段否则对端网卡可能会丢弃长度错误的报文。书里有一个调试技巧值得记下来在转发链路上打印mbuf-pkt_len和mbuf-data_len如果两者不一致说明报文被分段了而rte_eth_tx_burst对分段报文的处理依赖 offload 标志设置不对就会丢包。4.3 多队列与 RSS把单核瓶颈拆成多核吞吐单个 CPU 核的收包能力是有限的通常用在 1.25Mpps 到 2Mpps 之间再高就会出现收包延迟或丢包。要突破这个限制必须启用多队列。DPDK 支持的队列数取决于网卡型号rte_eth_dev_info_get可以查询。设置多队列之前先确认网卡驱动支持 RSSReceive Side Scaling——它能让网卡根据报文的五元组哈希自动分散到不同队列。// 配置端口为 4 个接收队列 struct rte_eth_rxconf rxq_conf port_conf.rx_adv_conf.rx_conf; for (q 0; q 4; q) { ret rte_eth_rx_queue_setup(portid, q, 1024, rte_eth_dev_socket_id(portid), rxq_conf, mbuf_pool); }队列数量一旦多于 CPU 核数就必须用rte_eth_dev_rss_hash_update配置哈希类型和 key让不同队列落到不同核上。这里有一个坑rte_eth_rx_queue_setup的第三个参数是描述符数量它必须是 2 的幂次方这一点和 rte_ring 一样。我遇到过同事把描述符配成 1000结果网卡初始化时直接失败。书里建议的起步值是 1024如果转发路径长、延迟高可以试着加大到 2048但注意每个描述符对应一个 mbuf内存占用会增加需要同步调整内存池大小。4.4 队列深度的实践判断从理论值到实测血泪不要以为队列深度配得越大越好。队列深度大意味着网卡能缓存更多包但也会让报文在队列里的等待时间变长延迟随之上升。低延迟场景比如高频交易或音视频转发队列深度 512 通常就够了高吞吐场景比如流量重放或日志汇聚2048 更稳。书里的经验是队列深度的选择看丢包率曲线而不是看理论带宽。具体做法是固定 CPU 频率从 512 开始打流逐步提高 PPS观察丢包拐点如果 512 队列在 1.5Mpps 时就出现丢包换 1024 通常能顶到 2Mpps。超过 2Mpps 还丢包问题往往不在队列深度而在 CPU 核心数不够或内存访问跨 NUMA。5. DPDK 落地避坑从安装到收包链路的 5 个经典翻车案例5.1 现象绑定网卡后dpdk-devbind.py --status显示 Interface not found原因这张网卡被内核驱动占用而且当前有网络会话挂载--bind操作被系统拒绝或者网卡的 PCI 地址写错查成了0000:02:00.0但实际设备是0000:02:00.1。解决先ip link show查看所有网卡的对应关系必要时把网卡 down 掉再绑定ip link set enp2s0 down dpdk-devbind.py --bindvfio-pci 0000:02:00.05.2 现象程序刚启动就报EAL: No available hugepages并退出原因大页内存没有实际分配成功/mnt/huge挂载了但/sys下的 nr_hugepages 写不进去或者系统开了 ASLR 导致 DPDK 映射失败。解决确认/proc/meminfo里的 HugePages_Total 非零如果为零手动执行一次echo 4 /sys/devices/system/node/node0/hugepages/hugepages-1048574kB/nr_hugepages执行前先用umount /mnt/huge解除挂载。还有一点Docker 容器里跑 DPDK 时宿主机必须把大页内存透传进容器很多人在容器里折腾半天其实问题在宿主机。5.3 现象l2fwd 收包计数有增长但转发出去的包对端收不到原因端口掩码配置错误-p 0x3时两个端口都工作但 l2fwd 默认根据目的 MAC 决定从哪个端口发出去如果对端机器连接在端口 1 上而报文的 MAC 对应的转发目标是端口 0报文就被从错误的口发出去了。解决先用rte_eth_macaddr_get打印两个端口的 MAC再用tcpdump -i any在对端确认包有没有上线路。测试 l2fwd 最简单的方式是让对端机器同时连接两个端口从网卡 0 收、网卡 1 发并保证目的 MAC 指向对端网卡。另一个常见成因是没开--no-mac-updating标志l2fwd 会修改报文的源和目的 MAC修改后的 MAC 不被交换机接受也会导致丢包。5.4 现象收发队列数量配置超过网卡上限初始化报Invalid argument原因很多虚拟网卡比如 virtio-net只有 1 个队列物理网卡则各有上限。rte_eth_dev_info_get返回的max_rx_queues是实际能配置的上限超过这个值直接失败。解决把队列数改回上限以内。验证是否是多队列网卡用ethtool -l查看组合通道数。如果确认网卡支持但 DPDK 报错检查是不是没有正确设置 RSS 哈希——某些网卡要求先配置rte_eth_dev_rss_hash_update否则后端的多队列初始化进行不下去。5.5 现象高 PPS 打流时 CPU 占用 100%吞吐反而下降原因轮询模式本身就是用 CPU 换延迟单核收包超过能力上限后CPU 在中断、缓存、内存访问之间来回切换性能不升反降。解决先看是不是只有 1 个核在工作如果是启用 RSS 或按队列绑定多核然后再看perf top如果热点集中在rte_mempool_get和 memory barrier 上说明内存池的 cache_size 太小全局池锁竞争严重。把cache_size从 256 调到 512把队列描述符从 1024 调到 2048吞吐通常会回升。如果还不行就要审视代码里是不是有隐式的锁比如日志打印或统计计数用了printf。提示以上 5 个案例是 DPDK 学习路径上重复率最高的翻车点。遇到问题不要先怀疑 DPDK 本身优先检查环境把/proc/meminfo和dpdk-devbind.py --status两张图贴出来问题基本就暴露一半了。6. 读完这本笔记之后从 l2fwd 走向自己的转发框架到这里你已经能把 DPDK 跑起来、理解了基本收发包链路也知道了常见的坑。下一步是脱离示例程序搭建属于自己的转发框架。常见做法不是从零开始而是参考 DPDK 官方示例里更复杂的三层转发示例 l3fwd把它的路由表查表逻辑换成自己的业务逻辑。但在动手之前有一条验证建议给到你用pktgen-dpdk做压力测试而不是简单用iperf3打流。pktgen-dpdk 本身基于 DPDK可以生成线速流量直接看到你程序在不同 PPS 下的丢包曲线。只靠 iperf3 测带宽一旦达到线速你根本看不出程序还能扛多少余量。进阶代码路线上我建议先读rte_eth_tx_burst在网卡满载时的返回值处理。tx_burst并不能保证把传入的报文全部发出它会返回实际发送成功的数量剩余的要由你重新入队或者直接释放。很多人刚开始整天丢包不是收包不行而是发包侧没有处理失败的返回。正确做法是把发送失败的包收集起来下一次循环再尝试发送超过重试次数再丢。这个逻辑在 l3fwd 示例里有现成代码直接移植过来就好。还有一个容易被忽视但影响巨大的细节CPU 绑核。DPDK 程序跑起来之后必须用taskset或者程序内的rte_eal_remote_launch把收包、处理、发包绑定到固定核上。否则操作系统调度器会在多个核之间迁移线程缓存和 TLB 全部失效性能直接打回内核协议栈水平。绑核之后用htop确认每个 DPDK 线程的 CPU 占用稳定在 100% 左右是正常的如果看到线程在多个核之间跳动说明绑核没有生效。在实际业务中我吃过最大的亏是内存池太小导致高峰期丢包。当时以为 16384 个 mbuf 够用了结果突发流量一上来内存池被取空rte_pktmbuf_alloc返回空指针程序没做判空就直接用到收包函数里直接段错误。自此之后我养成了两个习惯第一是内存池数量按峰值 PPS 乘以 2 倍来配置第二是所有取 mbuf 的地方都必须判空哪怕这让代码难看一点。这两个习惯让我在后面的多次线上压测里都少折腾了好几个通宵。把《深入浅出DPDK》读完、把这里的示例跑通之后你手里的就不再是一个零散的概念集合而是一条「环境搭建 — 收包 — 内存管理 — 多队列 — 发包」的完整链路。剩下要做的就是把你自己的业务逻辑接到这条链路的中间环节上。希望这篇笔记能帮你少走一段我当时走过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站