ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

拆解MLX5网卡驱动:从队列机制到性能调优实践

拆解MLX5网卡驱动:从队列机制到性能调优实践 1. 为什么一个网卡驱动值得花时间拆解很多人一听网卡驱动四个字第一反应是这有什么好看的装好了能上网不就行了。直到你某天在数据中心里跑分布式训练或者搭一套高性能网关发现吞吐死活上不去、CPU中断打满、延迟抖动大得离谱才开始意识到网卡驱动在整个网络栈里的分量。MLX5 是 NVIDIA原 MellanoxConnectX 系列网卡和 BlueField DPU 的 Linux 内核驱动。它不是一个简单的发包收包程序而是一个横跨 Ethernet、InfiniBand、RDMA、virtio-net、vDPA 的复合型驱动。在我个人的划分标准里MLX5 属于驱动里的航空母舰——它既承载了普通数据中心的 TCP/IP 流量又承载了 HPC 领域里延迟敏感的 RDMA 流量还承担了 SmartNIC 场景下的硬件卸载工作。你很难再找到一个驱动能同时覆盖这么多条技术线。这篇文章适合谁看我大致分成三类做 Linux 网络性能调优的运维和 SRE。你需要知道哪些参数在驱动层面真实有效哪些是网上人云亦云的设置。做内核驱动开发和 RDMA 相关工作的工程师。你需要快速定位代码理解模块之间的调用关系。对高性能网络感兴趣、想搞明白 Netdev/DPDK/XDP 背后硬件原理的学生或研究者。我觉得 MLX5 是你绕不开的样本。我自己最早是被一个生产问题逼着去读 MLX5 代码的某台机器跑高并发 Nginx软中断 CPU 使用率 100%吞吐却不到预期的一半。当时我用 ethtool 调了 rx-usecs、调了队列数、改了 RPS/RFS效果都有限。后来翻了驱动的 IRQ 亲和性注册逻辑才发现问题出在 irqbalance 和驱动默认策略的配合上。从那以后我就养成了一个习惯任何一个网络性能问题都先翻驱动代码再动参数。这篇文章我就把几条我认为最关键的线索串起来讲。2. 从源码目录看 MLX5 的模块划分2.1 core 目录驱动的中枢神经系统MLX5 驱动代码在主线内核里的路径是drivers/net/ethernet/mellanox/mlx5/core/。这个名字里的core非常贴切因为整个驱动基本上围绕一个核心、多个前端来组织。你打开 core 目录会看到en_*.c和ib_*.c两类文件。en开头的文件实现的是 Ethernet netdev 功能也就是我们熟悉的eth0设备ib开头的文件实现的是 InfiniBand 和 RDMA 功能暴露出来的是/dev/infiniband/下的字符设备。这两类前端共享一套底层的硬件访问机制命令接口、事件通知、固件握手、队列管理。新手最容易懵的地方是为什么同一块网卡既能当普通的 Ethernet 网卡用又能当 RDMA 网卡用答案是硬件本身就被设计成多平面的。驱动通过 core 层与固件协商把硬件资源划分给不同功能模块。你如果看代码会发现mlx5_core_dev这个结构体贯穿始终它就是驱动的总管家里面保存着设备能力、固件版本、各个功能模块的私有数据指针。在 core 目录下还有几个值得留意的重要文件main.c负责 PCI 设备的 probe 和 remove 流程fs_core.c是 flow steering流表的核心实现eq.c处理事件队列cq.c和srq.c则实现了硬件队列资源的管理。我在阅读的时候建议的切入顺序是先读main.c的probe函数你会看到驱动初始化时一步步做了什么再顺着mlx5_load_one往下走就会碰到mlx5e_en和mlx5_ib两个模块的加载。2.2 en 与 ib两台面向不同客户的前端机器en前端的作用是让 Linux 网络栈能使用这块硬件。它注册了net_device_ops里的ndo_open、ndo_start_xmit、ndo_stop等回调函数。当你执行ip link set eth0 up时驱动会创建发送队列SQ、接收队列RQ、完成队列CQ以及相关的 DMA 资源。这里有一个很适合观察驱动的入口en_main.c里的mlx5e_open函数它就是把网卡从配置状态拉到运行状态的关键路径。ib前端则面向 RDMA 用户。它实现了 InfiniBand 子系统要求的ib_device接口注册 GID 索引、QP队列对操作、内存注册MR等回调。上层无论是用ib_verbs直接写程序还是通过rdma_cm跑 RoCEv2最终都会落到 mlx5_ib 的代码上。从性能角度看我们通常更关注en前端因为绝大多数流量还是走 TCP/IP 协议栈。但如果你做的是存储集群或 AI 训练集群ib前端的性能同样关键它直接决定了 RDMA 读写延迟和带宽。2.3 include 目录驱动与内核其他模块的契约别忘了include/linux/mlx5/这个目录。它里面放着 core 层导出的 API 接口定义。如果你要写一个依赖 MLX5 硬件能力的内核模块比如自定义的 TC offload 插件大概率会引用这里的头文件。我常把这个目录看作驱动对外的契约书接口命名和参数设计都比较规范适合作为阅读代码的辅助索引。3. 数据路径的主航道从 RQ/SQ 到 CQ 的代码实现3.1 接收队列RQ与发送队列SQ的硬件视角网卡驱动和普通 PCIe 设备驱动的最大不同是它必须认真对待队列这个概念。MLX5 的发送和接收路径上硬件和软件之间通过 WQWork Queue交互。驱动往发送队列里填 WQEWork Queue Element硬件消费它并把包发出去硬件把收到的包写进接收队列的 WQE驱动消费它并递交给内核协议栈。你最终在代码里看到的是一组mlx5e_sq和mlx5e_rq结构体它们是软件视角的队列对象。mlx5e_rq里包含wqe数组、DMA 映射信息、页面池page pool指针和回调函数。每次硬件收到包网卡 DMA 引擎直接把数据写到预先映射好的内存地址然后通过完成队列事件通知驱动去轮询。我在理解这段逻辑时参考了一个很生活的类比WQ 就像超市的传送带。收货区接收队列的传送带上摆好了空购物筐硬件把商品放进筐里驱动负责把筐端走发货区发送队列的传送带上驱动把商品摆上去硬件负责运走。如果两边速度不匹配传送带就会有瓶颈。驱动代码里大量复杂的逻辑本质上都是在调节传送带的节奏什么时候放筐、放几个筐、货满了怎么处理。3.2 CQ 与 NAPI 轮询中断的降噪完成队列CQ的作用是记录 WQ 里 WQE 的完成状态。每个发送或接收 WQ 都会关联一个 CQ。当硬件完成了某个 WQE 的处理就在对应的 CQ 里写入一条完成记录CQE。这就是为什么驱动性能调优时CQ 的数量和中断绑定关系如此重要——因为 CQ 中断就是驱动被叫醒的闹钟。在 MLX5 的 Ethernet 前端mlx5e_open_channel函数会创建一条通道channel每条通道由 1 个 RQ、1 个 SQ 和 1 个 CQ 组成并绑定到一个 CPU 核心。这种 1:1:1:1 的模型是整个驱动高性能的基础。en_main.c中mlx5e_open_channel会调用mlx5e_open_cq再调用mlx5e_open_rq和mlx5e_open_sq每个队列都会注册自己的 NAPI 实例。NAPI 是 Linux 网络子系统的一种中断节流机制。我第一次弄懂 NAPI 时觉得这个设计特别聪明收包时先让中断触发一次之后进入轮询模式用net_rx_action在软中断上下文里批量处理包直到处理完所有包或者达到预算上限再重新开启中断。MLX5 驱动在en_rx.c的mlx5e_poll_rx_cq里实现了这个轮询逻辑配合mlx5e_deactivate_rq等函数管理队列的启停。3.3 驱动收包路径的完整链路如果你跟我一样喜欢沿着一条包走到底的读代码方式我建议你把下面这条链路在源码里完整走一遍硬件收到包写入 RQ 的 WQE。硬件生成 CQE写入对应的 CQ同时触发中断如果中断没有屏蔽。中断处理函数mlx5e_irq_handler被调用它最终激活 NAPI。软中断上下文执行mlx5e_napi_poll调用mlx5e_poll_rx_cq取出一批 CQE。对每个 CQE驱动从 RQ 的 WQE 中拿到 DMA 地址准备构建 SKB。mlx5e_build_skb根据配置决定是线性化还是非线性化调用napi_gro_receive把包送入协议栈。从第 5 步到第 6 步之间有一个经常被忽略的性能分水岭驱动是否启用了MLX5E_RQ_STATE_MINI_CQE_ENABLE、是否使用page_pool分配页面、是否把数据从多个页面片段组装成 GRO 聚合包。这些细节在代码里体现为不同的注册函数指针。我在性能分析时经常通过perf查看mlx5e_poll_rx_cq内部的热点如果发现build_skb和page_pool相关函数占用过高就说明内存分配和 SKB 组装路径不够理想。4. 性能优化从硬件中断到 CPU 亲和性4.1 中断绑定看起来简单坑却最多MLX5 网卡性能调优的第一课就是中断绑定。每一条通道的 CQ 中断会被分配到某个 CPU 核心上让这个核心专门负责处理该通道所有收发包的软中断。这样做的好处显而易见避免了 CPU 之间的缓存颠簸cache ping-pong让数据面保持在本地核心上运行。驱动在初始化时会根据 PCIe MSI-X 中断向量的个数来创建对应的通道。默认策略下每个通道的中断会按顺序映射到不同的 CPU但irqbalance服务往往会干预这个策略。我在实际环境中遇到过多次irqbalance把网卡中断从一个核心搬到另一个核心导致吞吐突降。把irqbalance停了之后性能立刻恢复。这里我给一个比较稳妥的操作方案用ethtool -L eth0 combined 8设置网卡队列数量。队列数量一般不超过 CPU 物理核心数。查看/proc/interrupts找到网卡对应中断号。把每个中断号绑定到不同 CPU例如echo 2 /proc/irq/125/smp_affinity2 表示 CPU1。关闭 irqbalancesystemctl stop irqbalance。为什么希望队列数和 CPU 核心数一致因为每条通道的 NAPI 轮询只会跑在它绑定的核心上。如果队列数少于 CPU 数部分核心空闲如果队列数多于 CPU 数会面临多个队列抢一个核心的情况但这时绑定策略就变成了哪个队列优先级更高的问题。一般情况下combined的数量等于物理核心数或 NUMA 节点内核心数即可这个结论我是在多台不同型号服务器上验证过的。4.2 中断合并coalescing延迟和吞吐的零和博弈中断合并是另一个核心调优点。MLX5 驱动通过ethtool -C eth0 rx-usecs N和tx-usecs N来控制硬件中断合并的时间窗口。简单来说网卡收到包之后不会立刻发中断而是等累积了一定数量rx-frames或等待了一定时间rx-usecs再通知驱动。这个机制对高吞吐场景非常友好一次中断处理几百个包CPU 利用率大幅降低。但对延迟敏感场景就不合适了因为每个包都要多等一个合并窗口才被处理。我在多台机器上做过的对比测试结果大致如下表数值因具体硬件型号和内核版本可能有差异配置场景效果rx-usecs0, rx-frames0低延迟优先延迟稳定但 CPU 占用明显上升rx-usecs4, rx-frames8均衡配置大多数场景下表现均衡rx-usecs32, rx-frames16高吞吐优先pps 高但 TCP 小包延迟显著增加一个值得注意的细节是当你用 DPDK 或 AF_XDP 收包时中断合并的意义不大因为用户态程序是主动轮询的。但我见过有人把 DPDK 网卡的中断合并参数也调得很高这属于典型的拿着错误的旋钮在转。4.3 硬件卸载的开关RSS、GRO、GSO、LRO现代高速网卡把一部分协议处理任务从 CPU 搬到了硬件。MLX5 支持 RSSReceive Side Scaling、GROGeneric Receive Offload、GSOGeneric Segmentation Offload、LROLarge Receive Offload等特性。RSS 的实质是硬件根据包头的哈希值把流量分发到不同的接收队列。驱动通过ethtool -X eth0 equal 8来设置哈希队列的权重。生产环境中我一般建议开启 RSS让它按四元组哈希均匀分发到各队列。但这里有一个看起来比较隐蔽的问题如果开启了 GRE/VXLAN 隧道哈希分发的目标可能是外层头而不是内层头这样隧道流量的分发效果就会变差。MLX5 驱动针对 VXLAN 提供了专门的哈希配置这个能力是通过 flow steering 来实现的。GRO 和 GSO 的作用则是把多个小包合并成大包再交给协议栈或者把大包分段发送。对于跑大文件传输、视频流的场景这两个特性对吞吐提升非常明显但对延迟敏感的金融行情、在线游戏后端gro 的合并会引入额外延迟需要你根据业务实际情况权衡。我在折腾这些参数时的经验是先搞清楚瓶颈在哪再决定动哪个旋钮。如果 CPU 的软中断占用已经接近 100%优先考虑队列数量和 RSS 分发如果带宽上去了但单流吞吐低优先考虑 GRO/GSO如果延迟抖动大优先考虑关掉中断合并或者减小合并窗口。5. 不同场景不同的性能调优组合5.1 基准测试场景怎么把 pps 打到最高如果你是做性能测试的目标很简单在特定包大小下跑出最高的 pps包每秒或者带宽。这个目标通常需要把中断合并开到较大窗口把队列数设为满还要关掉 NAPI 相关的节流机制。在mlx5e_napi_poll中有一个预算机制budget默认值通常是 64。这意味着每次 NAPI 轮询最多处理 64 个包之后软中断会重新调度。你可以在驱动代码里看到它对budget的使用逻辑如果处理完所有 CQE 但还不到 budget说明队列已经处理完可以退出轮询如果达到 budget 但 CQE 没处理完说明负载较重但尽量避免长时间占用 CPU。对纯基准测试我常用的方法是使用pktgen脚本生成高速发包流。设置ethtool -C eth0 rx-usecs 8 rx-frames 16。用 taskset 把出入包流量打在不同 CPU 上。观察/proc/interrupts的中断分布和mpstat的软中断占比。如果你发现单核软中断已经打满但吞吐还是上不去最可能的原因是队列数不够或者哈希分配导致某个队列负载不均。此时可以用ethtool -X eth0 equal 8重新分配哈希权重。5.2 生产环境的高并发小包场景高并发小包场景是驱动调优里最有挑战性的一种。典型特征是连接数多、单包很小例如几十字节到两百字节、并发活跃连接数上万。这种情况下CPU 消耗的很大一部分来自协议栈和驱动本身对每个包的开销而不仅仅是数据复制。我通常会做以下几件事关闭 GRO 合并的延迟代价如果连接状态需要极低延迟就关闭 GRO 或调小合并窗口。使用 RFSReceive Flow Steering让同一连接的包尽量在同一 CPU 上处理。MLX5 支持基于内核rps_flow_cnt和rps_sock_flow_entries的 RFS 配置可以降低锁竞争。适当调大netdev_budget和netdev_budget_usecs让每次 NAPI 轮询可以处理更多包减少唤醒次数。如果业务允许直接用 XDP 在驱动层就丢掉不需要的包。MLX5 驱动对 XDP 的支持在en_xdp.c中它可以做到在 SKB 构建之前直接处理包可以大幅提升小包场景的吞吐。提到 XDP这里展开说一句。MLX5 的 XDP 路径非常值得研究驱动在mlx5e_poll_rx_cq处理 CQE 时会先检查 RQ 是否启用了 XDP 程序。如果启用了就直接在数据页上运行 BPF 程序根据返回结果决定是转发、丢弃、重定向还是上送协议栈。这个先 XDP 再 SKB的顺序意味着很多无用的包根本不会进入协议栈这在高防御、高过滤场景下是最有效的手段。我个人在生产环境里做过一次 XDP 拦截非法 IP 的测试在未开启 XDP 时单核处理约 35 万 ppsCPU 软中断已经打满开启一个简单的 drop XDP 程序后单核可以跑到100万 pps 甚至更高CPU 占用还有剩余。这就是硬件和驱动配合的效率优势。5.3 云环境与虚拟化场景MTU、vDPA 与 SF云环境下你用的可能不是完整的 ConnectX 网卡而是通过 SR-IOV 切出来的 VFVirtual Function或者是通过 vDPA 虚拟化出来的 virtio-net 设备。MLX5 驱动在 vDPA 路径的实现位于drivers/vdpa/mlx5/它让 VM 可以直接访问硬件队列实现近原生的性能。如果是在虚拟化场景下做性能优化MTU 和队列协商是两个最要紧的变量。虚拟化环境下经常出现 MTU 不一致导致的分片问题这会让驱动和硬件被迫处理大量额外开销。另一个容易踩的坑是VF 的队列数往往受限于 PF 上配置的资源。你在宿主机上用mlx5_core的 devlink 接口配置资源分配时要同时考虑所有 VF 的需求。6. RDMA 与硬件卸载MLX5 真正拉开差距的地方6.1 mlx5_ib 的 QP 与内存注册机制普通网卡驱动把数据从网卡搬到内核协议栈就完事了但 MLX5 的 InfiniBand 和 RoCE 路径可以直接把网卡数据搬到用户态应用程序的缓冲区中间不需要经过内核。这就是 RDMA 技术中内核旁路的核心也是mlx5_ib模块存在的原因。RDMA 的发送和接收流程和 Ethernet 路径完全不同。应用创建 QPQueue Pair包括一个发送队列和一个接收队列然后向驱动注册内存区域MR把用户态虚拟地址映射到硬件可以访问的物理地址。mlx5_ib里的mlx5_ib_post_send和mlx5_ib_post_recv是用户态发起的操作它们直接把 WQE 写入硬件队列再通过 doorbell 寄存器通知硬件。我在看 RDMA 代码时最喜欢的一个函数是mlx5_ib_mr_alloc它负责内存注册也就是把虚拟内存变成硬件可寻址的内存区域。这个过程中最关键的是页表映射和缓存失效处理如果做不好性能会很差。Mellanox 的工程师在内核里做了大量优化比如使用了 umem 缓存、增加了大页支持等。6.2 devlink 与 SFSub-function网卡资源的软件化切分当前版本内核里MLX5 驱动对 devlink 的支持已经比较成熟。你可以通过 devlink 查看设备的参数、上报健康状态、控制 FW 升级。devlink health是排查网卡异常非常有用的工具MLX5 驱动实现了多个 reporter比如fw_reporter和tx_reporter。当网卡出现异常降低性能时健康 reporter 会输出相关信息这在生产环境里是我排障的第一个入口。SFSub-function是一个比较前瞻的概念在同一个物理 PF 下可以创建多个具有独立功能的子功能每个 SF 可以绑定不同的驱动实例同时共享同一个硬件。MLX5 的 SF 支持在drivers/net/ethernet/mellanox/mlx5/core/sf.c中实现它依赖于mlx5_core的设备实例化能力。如果你在规划高性能容器网络或 Serverless 基础设施SF 是个值得研究的替代 SR-IOV 的方案。7. 调试与排障我的第一反应是看这些数据作为一个长期和网络性能问题打交道的从业者我总结了一套遇到 MLX5 网卡问题时的排查路径分享给大家参考先看ethtool -S eth0里有没有明显递增的错误计数。重点关注rx_crc_errors_phy、rx_symbol_err_phy、tx_dropped、rx_dropped。这些字段直接反映了物理层和队列层的问题。用ethtool -d eth0导出寄存器信息判断是否存在链路降速、PCIe 带宽不足等硬件问题。查看 dmesg 里有没有 mlx5 相关的异常日志。fw reset事件和 CQ 超时错误通常会直接出现在日志里。在确认硬件正常之后再去看中断分布、队列映射和 CPU 软中断占比。有一个我反复踩过的坑PCIe 降速。物理链路是 100GbE但因为 PCIe 插槽的 lane 数不够或者 PCIe 链路降速网卡实际带宽只有 50G。这种问题从网卡侧看可能毫无异常但吞吐就是上不去。所以我会用lspci -vvv检查LnkSta的速率和宽度确认 PCIe 链路没有降速。如果你收到mlx5_core 0000:XX:00.0: CQ timeout之类的报错通常是固件或 DMA 映射出现问题排查思路要转向内存、IOMMU 和固件版本。这个时候千万不要只盯着网络参数要结合完整日志来定位。8. 我个人的经验学驱动先学思路再学参数写到这里我想分享一点更宏观的感受。很多人学驱动优化喜欢直接收集一堆 sysctl 和 ethtool 命令这当然有用但如果没有理解驱动工作的底层逻辑遇到新问题还是会手足无措。MLX5 驱动的代码结构是一个特别好的学习样本因为它把硬件能力和软件策略分得很清楚。你读懂了mlx5e_channel的创建过程就理解了为什么队列数是性能调优的第一步你读懂了 CQ 和 NAPI 的配合就理解了为什么中断合并是延迟和吞吐之间的杠杆你读懂了 RQ 的 WQE 和 page pool 机制就理解了为什么内存分配策略会影响小包收包性能。我最推崇的学习路径是这样的先花一个下午完整读完en_main.c里mlx5e_open和mlx5e_open_channel函数把队列创建、中断注册和 NAPI 初始化的流程画下来。然后带着问题去看en_rx.c和en_tx.c的热点函数理解收发包的数据流。最后再去看en_ethtool.c里的参数设置函数你会惊讶地发现 ethool 里的每一个参数都能在驱动的代码里找到对应的实现逻辑。当你能做到看到一个参数就能在脑内浮现它对应的代码路径和硬件行为时性能调优对你来说就不再是玄学了。最后再分享一个小技巧如果你身边的机器支持多准备几块不同固件版本的 ConnectX 网卡对照测试。同一个 ethtool 参数在不同固件版本下的效果可能有微妙差别。固件升级不仅是修 Bug有时候它直接改变了默认的行为策略。把固件版本、驱动版本、内核版本三者一起纳入你的性能对比表格长期积累下来你会拥有一份非常宝贵的经验数据库。
返回列表