ARTICLE · 1338459
网卡如何提高网络吞吐:队列、卸载、旁路与总线约束
导论
网络吞吐从来不是「网卡更猛」这一个变量,而是四层天花板里最矮的那一层决定的:物理链路能跑多少、网卡内部能并行多少、主机接口(PCIe/DMA/内存)能喂多少、软件栈每包要花多少 CPU。这四层的量级差得很远——链路已经到了 800 Gb/s,PCIe 5.0 x16 单向只有约 63 GB/s,而一颗核跑完整协议栈每秒只能处理一两百万个小包。
本文按「先算账、再逐层拆」的顺序展开:第一章把四个约束换算成具体数字(pps、纳秒、GB/s、核数),确立「小包才是吞吐的真正考场」这一前提,并说明 FEC 如何吃掉标称带宽;第二章讲并行,多队列、RSS、MSI-X 与中断亲和,并点明全文反复出现的硬约束——同一条流永远落在同一个队列——以及发送侧对偶的 XPS 与 qdisc 锁;第三章讲如何少打断 CPU,硬件中断合并与自适应 DIM、软件侧的 gro_flush_timeout,以及吞吐-延迟曲线;第四章讲把逐包工作交给硬件,校验和、TSO/GSO、LRO/GRO、VLAN、隧道的卸载家族与各自的陷阱;第五章讲少搬一次内存,描述符环水位、DMA 与 IOMMU 的隐性开销、巨帧的真实收益与代价,以及链路层流控(pause/PFC)在「丢包」与「反压」之间的取舍;第六章讲把网卡变成「半个 CPU」,SR-IOV、TC flower 硬件流表、SmartNIC/DPU 各自卸载什么、代价是什么;第七章讲绕开内核的四条路——RDMA、DPDK、AF_XDP、XDP,以及它们各自适合谁;第八章讲横向扩展,链路聚合为什么救不了单流、多路径与拓扑收敛比;第九章给出一套分层测量方法,以及六个最常见的「测出来不对」的假象;第十章用一张取舍矩阵把技术按场景组合起来,收尾回答「什么时候该换架构」。
一句话主线:提升吞吐只有三种动作——把逐包成本变成批量成本(合并、巨帧、批量描述符),把 CPU 的活搬到硬件(卸载、流表、DPU),把单核串行变成多核/多设备并行(多队列、RSS、聚合、旁路)。 其余技术都是这三件事的变体。
一、先把账算清楚:四个硬约束
1.1 线速与包长:pps 与 Gbps 是两个坐标
同样 100 Gb/s,64 B 小包是 148.8 Mpps,1518 B 大包只有 8.1 Mpps,相差 18 倍。所有「吞吐」讨论必须先锁定包长,否则数字没有意义。
还要扣掉线路编码与 FEC 的开销,而且不同 PMD 扣的不一样:
别把 64b/66b 的 3% 与 FEC 叠加扣两次:标称的 10G/25G/100G 指的就是 MAC 净荷速率,64b/66b 已经体现在线路波特率里(10GbE 对应 10.3125 GBd)。所以「100G 口实测 94 Gb/s」是 DR/FR 类模块的正常上限,换成 SR4 模块能多出约 3 Gb/s——差距来自 FEC 方案,不是网卡配置。
1.2 CPU 的账:一颗核能吃多少 pps
一个包走完整内核路径(中断、NAPI、skb 分配、四层解析、socket 查找、拷贝到用户态)的典型成本是 300–800 ns,即单核 1.2–3 Mpps。把这张账和上表对齐:
结论很直接:大包场景瓶颈在链路和 PCIe,小包场景瓶颈在 CPU。 小包要跑到线速,靠加核是条死路(核数、内存带宽、锁竞争都会先崩),必须卸载、合并或旁路。
1.3 总线账:PCIe 代际与速率的匹配关系
PCIe 是每方向独立的通道,而以太网是全双工,所以「网卡单向吞吐」必须和「PCIe 单向带宽」对齐比。
注意口径:3.0~5.0 三行已扣掉 128b/130b 的开销,6.0/7.0 两行给的是编码前原始速率——FLIT 头与轻量 FEC 还要再吃掉 5%–8%,所以 6.0 x16 实际可用约 120 GB/s 而不是 128。
由此得到两条很实用的排障经验:
「标称 200G、实测 100G 出头」多半是总线问题,但要先分清单口还是双口。 单口 200G 需要 25 GB/s 单向:PCIe 4.0 x16(31.5 GB/s)够用但余量只剩四分之一,插槽降到 x8(15.7 GB/s)就只够约 126 Gb/s——这正是「200G 卡跑出 100G 出头」的典型成因;双口 2×200G 需要 50 GB/s,4.0 x16 根本喂不饱,必须上 5.0 x16(63 GB/s)。 800G 网卡在 PCIe 5.0 平台上物理上跑不满。 这也是 800G 网卡普遍配套 PCIe 6.0 平台的原因(PCIe 7.0 规范已于 2025 年 6 月发布,128 GT/s,x16 双向 512 GB/s)。
1.4 内存与 NUMA:看不见的第三个天花板
网卡写内存、CPU 读内存,同一份数据在内存里至少被访问两次;100 Gb/s 单向意味着约 12.5 GB/s 的持续 DMA 写入,叠加协议栈读取和 socket 拷贝,对内存带宽和 LLC 的压力远超直觉。跨 NUMA 访问会把延迟抬高 1.5–2 倍、带宽近乎减半,所以队列中断、DMA 内存、应用线程必须同 NUMA node,否则多队列再多也会被远端内存拖住。
1.5 软件栈的固定成本(一句话)
系统调用、skb 生命周期、分层解析、锁、唤醒——这些每包固定开销是小包吞吐的死敌。
二、并行:多队列、RSS 与中断向量
2.1 队列是网卡的并行单位
一个队列(queue)由三样东西绑在一起:独立的描述符环(DMA 收发环形缓冲)、独立的中断向量(MSI-X)、独立的 NAPI 实例(内核侧轮询上下文)。理想情况三者 1:1——MSI-X 向量不够时多个队列会共享一个向量,但每个 RX 队列仍有自己的 NAPI 实例;所以「队列数」大体就是网卡的并行度。
ethtool -l eth0 # 查看当前/最大 combined、rx、tx 队列数ethtool -L eth0 combined 16队列数不是越多越好,受三个上限约束:MSI-X 表项(PCIe 规范上限 2048 向量,网卡通常实现 64–256)、网卡内部缓存与调度资源、每个队列的描述符环内存。实践中队列数 ≈ 参与处理的核数最划算;队列远多于核数时,多个队列抢同一个核,反而增加缓存抖动和调度开销。
2.2 RSS:把不同的流散到不同队列
RSS(Receive Side Scaling)是网卡侧的硬件分流:对报文的四元组(可含 L4 端口)做 Toeplitz 哈希,用一张 indirection table(典型 128 项)把哈希值映射到队列号。哈希 key 通常 40 字节,可由驱动/ethtool 设置。
ethtool -x eth0 # 查看 indirection table 与 hash keyethtool -X eth0 equal 16 # 均分到 16 个队列ethtool -X eth0 weight 8 8 4 4 # 按权重分配RSS 带来三个必须记住的结构性限制:
同一条流永远落在同一个队列。 这是为了保序(乱序会被 TCP 当成丢包),代价是单流吞吐上限 = 单队列 + 单核的处理能力。哪怕有 64 个队列,一个 TCP 连接也只能用一个。这条限制解释了后面几乎所有「单流打不满」的现象。 哈希只覆盖网卡能解析到的层。 普通 IP 报文没问题;但 VXLAN/Geneve 这类隧道,若不开启内层哈希(需要网卡支持,对应 ethtool 键 rx-udp_tunnel-port-offload,net/ethtool/common.c:69,以及驱动特定的 tunnel RSS 配置),整条隧道会退化成「按外层五元组哈希」——一条隧道一个队列,多租户场景下极易打爆单核。在线改 indirection table 会让在途流短暂乱序。 ethtool -X重排映射后,同一条连接的新包可能落到新队列,与旧队列里还没处理完的包乱序,TCP 会当成丢包而掉窗。改表尽量在低峰一次改到位,别在压测中途反复调。
另外,做 NAT/负载均衡的中间设备建议使用对称哈希(源/目的互换后哈希一致),否则一条连接的两个方向可能落在不同队列、不同设备,会话状态和队列水位都对不上。
2.3 把队列钉到核上
grep eth0 /proc/interrupts # 看每个队列的 MSI-X 向量号与分布ethtool -S eth0 | head # 队列级统计,确认队列在工作irqbalance 在吞吐场景往往帮倒忙(会把同一网卡的队列摊到跨 NUMA 的核上)。常见做法是关掉 irqbalance,按队列手工写 /proc/irq/<n>/smp_affinity,或由驱动/脚本一次性绑定;同时确认每一列中断计数大致均匀——某一列远高于其他,就是哈希不均或分流失效。
2.4 硬件定向:flow director / ntuple
RSS 是「按哈希平均」,而 ntuple 过滤是「按规则精确投递」:
ethtool -N eth0 flow-type tcp4 src-ip 10.0.0.1 dst-port 3306 action 3ethtool -n eth0 # 查看已下发的规则用于把关键流(数据库复制、存储流量、交易行情)钉到专用队列/专用核,把控制面与数据面隔离。规则命中优先级高于 RSS;表容量通常只有几百到几千条,且规则不匹配时会回落到 RSS,这点在排障时很容易被忽略。
2.5 软件侧的补位(结论)
队列少于核数时,靠内核的 RPS/RFS/aRFS 在软件层二次分流;aRFS 还能让网卡把「某条流该去哪个核」的映射回报给硬件,形成硬件加速的 RFS。
2.6 发送侧的并行:XPS 与 qdisc 锁
RSS 只解决收包。发送方向能不能多核并行,取决于另外两件事,而它们经常被忽略——这也解释了为什么「收能打满、发打不满」:
XPS(Transmit Packet Steering):用一张 CPU 掩码决定「哪些核允许用哪个 TX 队列」,入口是 /sys/class/net/eth0/queues/tx-*/xps_cpus,内核侧netif_set_xps_queue()(include/linux/netdevice.h:4107)。没配 XPS 时,多个核会集中往少数 TX 队列里塞包,在队列锁上互相等。qdisc 的串行化:每个 netdev_queue有一把_xmit_lock(netdevice.h:709);非 lockless 的 qdisc 还要靠qdisc_run_begin()的 running 位保证同一时刻只有一个 CPU 在跑这个 qdisc(include/net/sch_generic.h:202)。只有带TCQ_F_NOLOCK的 qdisc(sch_generic.h:92,noqueue与mq/mqprio的叶子队列属这类)才真正允许多核同时发包。
tc qdisc show dev eth0 # 期望看到 mq / mqprio,而不是单个 pfifo_fastcat /sys/class/net/eth0/queues/tx-*/xps_cpus # 每个 TX 队列允许哪些核echo f > /sys/class/net/eth0/queues/tx-3/xps_cpus # 掩码 f = 只允许 CPU 0-3这与 RSS 是同构的:收靠 RSS 把流分到队列,发靠 XPS + 多队列 qdisc 把流分到队列,两边都被「同一条流只走一个执行单元」锁住。所以单流双向打满靠的是单队列 + 单核的能力,堆核数没用。
三、少打断 CPU:中断合并与自适应
3.1 中断的真实成本
100G 小包按「每包一次中断」算是 1.49 亿次/秒。一次中断的进入/退出、寄存器保存、缓存污染合计 1–2 µs,算下来需要 150–300 秒 CPU 时间才能服务 1 秒的流量——物理上不可能。中断合并(interrupt coalescing)是让这条路可行的前提。
3.2 参数语义
ethtool -c eth0ethtool -C eth0 rx-usecs 16 rx-frames 64 adaptive-rx onrx-usecstx-usecs | ||
rx-framestx-frames | ||
rx-usecs-irqtx-usecs-irq | 中断正在被主机服务期间include/uapi/linux/ethtool.h:404-408) | ethtool -c 里显示 n/a |
adaptive-rxadaptive-tx |
usecs 与 frames 是谁先到谁触发:小包高 pps 场景下 frames 先到,大包低 pps 场景下 usecs 先到。合并之后,单队列中断从「百万级/秒」降到「几万级/秒」,中断成本从主导项变成零头。
内核侧的自适应实现是 DIM(lib/dim/net_dim.c):它把「每包处理成本」和「每次中断成本」建模,在吞吐与延迟之间做二分调整。
3.3 吞吐-延迟曲线:这里没有免费午餐
rx-usecs 32 rx-frames 64 adaptive-rx on | ||
adaptive-rx on | ||
rx-usecs 0 rx-frames 0)+ SO_BUSY_POLL |
高吞吐与低延迟在这条曲线上是相反方向,这是网卡调优最基本的取舍,任何「既要又要」的方案(如自适应 DIM + 定时轮询)本质是在曲线上找折中点。
3.4 busy poll:用 CPU 换延迟
net.core.busy_poll(微秒)与 SO_BUSY_POLL 让 socket 在等数据时直接轮询网卡队列,跳过「睡眠—中断—唤醒」这条链路。它属于低延迟手段,在高吞吐场景会烧掉整核,与上一节的中断合并是两种相反的策略。
轮询把整核占住,所以还要配套两件事,否则省下的延迟会被抖动吃掉:把核隔离出去(isolcpus=、nohz_full=,kernel-parameters.txt:2739、:4513),以及关掉深度 C-state——CPU 一旦进 C6,退出延迟是几十微秒量级,轮询探到的「空闲」反而更贵。
3.5 第三层:软件中断合并(gro_flush_timeout + napi_defer_hard_irqs)
硬件合并之外,NAPI 里还能再做一层「合并中断」:把硬中断推迟几次轮询,改用高精度定时器批量 flush GRO。两个旋钮挂在网卡级 sysfs:
cat /sys/class/net/eth0/napi_defer_hard_irqs # 连续多少次轮询内有工作量,就继续不开中断cat /sys/class/net/eth0/gro_flush_timeout # 单位 ns:定时器到点才 flush GRO语义在 net/core/dev.c:6776-6803:一轮 poll 有工作量时,先把 defer_hard_irqs_count 重置为 napi_defer_hard_irqs;只要它仍大于 0,就把 IRQ 保持关闭、改由 gro_flush_timeout 到点触发 gro_flush_normal()。内核给软件设备准备的默认值就在 netdev_sw_irq_coalesce_default_on()(dev.c:11982-11988):20000 ns + defer 1。
它比硬件合并更省中断——因为合并发生在 GRO 之后,一次定时器能带出一大批包,而不是按「帧数/微秒」提前上报。代价是延迟直接由 gro_flush_timeout 决定:调成 100 µs 以上,单包延迟就跟着走。这是 100G 收包压软中断的常用手段,也是「中断数降到几万/秒、但尾延迟变差」的主要原因之一。
四、把逐包工作交给硬件:卸载家族
4.1 一张表看清 offload
ethtool -k eth0 # 全量特性表ethtool -K eth0 <feature> on|offrx-checksumrx-checksumming) | net/ipv4/tcp_offload.c:420-426) | |||
tx-checksum-ip-generic | ||||
tx-tcp-segmentationtcp-segmentation-offload,TSO) | ||||
tx-udp-segmentation | ||||
rx-lrolarge-receive-offload,LRO)/ rx-gro-hw | 转发/路由场景禁用 | |||
tx-scatter-gatherscatter-gather) | ||||
rx-vlan-hw-parsetx-vlan-hw-insert(≤6.x:rx-vlan-offload / tx-vlan-offload) | ||||
rx-udp_tunnel-port-offload | ||||
rx-hashing | ||||
hw-tc-offload | ||||
esp-hw-offloadtls-hw-tx-offload、tls-hw-rx-offload |
⚠️ 7.x 把一批特性字符串改成了 tx-* / rx-* 前缀(net/ethtool/common.c:14-75,并非全部——hw-tc-offload、esp-hw-offload、rx-hashing 保持原样):rx-checksumming→rx-checksum、tcp-segmentation-offload→tx-tcp-segmentation、large-receive-offload→rx-lro、scatter-gather→tx-scatter-gather、rx-vlan-offload→rx-vlan-hw-parse。ethtool -k 打印的就是这张表,-K 也按这串名字匹配,所以在 7.1 上写旧名会匹配不上、直接报错。动手前先跑一遍 ethtool -k,照本机打印的名字写。
4.2 校验和卸载的三种语义(一句话版)
收包侧硬件验证过 → CHECKSUM_UNNECESSARY(GRO 直接跳过复算);硬件给出整包校验和 → CHECKSUM_COMPLETE;发包侧「从某偏移开始由硬件算」→ CHECKSUM_PARTIAL。这三个状态贯穿全栈,是理解「为什么关掉 rx-checksum 吞吐掉一半」的关键。
4.3 LRO 与 GRO:名字像,用途完全不同
GRO 是软件合并(内核 NAPI 里做),可以按需关闭、可以保留足够信息; LRO 是硬件合并,把多个同流报文合成一个超大包再上交,省 CPU 最狠,但破坏了「一包一决策」的语义:转发/路由/防火墙需要逐包看头部,合并后时间戳、校验和、TTL 语义都会失真,所以这类设备上必须关闭 LRO(或改用 rx-gro-hw,它在保留语义的前提下做硬件 GRO)。
判断标准很简单:这台机器是流量的终点还是中转站? 终点可以开 LRO,中转站不能。
4.4 TSO 的收益为什么这么大
没有 TSO 时,一个 64 KB 的发送窗口要拆成约 44 个 MSS 段,每段各走一次驱动入口、一次 doorbell(写网卡寄存器)、一次 DMA 映射。有 TSO 时应用写一个大 skb,网卡自己切分并逐段补校验和——CPU 侧的调用次数从「段数」变成 1。这就是「大包吞吐看 PCIe,小包吞吐看卸载」的由来:TSO 优化的是发送侧每字节的固定成本,而不仅是带宽。
4.5 卸载的边界
卸载不是魔法,只是把成本从 CPU 搬到硬件:硬件队列深度、规则表项、描述符格式都是有限的;而且卸载会削弱可观测性——抓到的包不再是线路上真实的包。生产环境建议保留一套「关闭 LRO/TSO 的观测口」,否则很多问题在抓包层面根本看不见。
五、少搬一次内存:描述符环、DMA 与巨帧
5.1 描述符环是「在途容量」,也是第二个串行点
每个队列的 ring 就是它能容纳的在途包数。环太小 → 突发时来不及回收,硬件直接丢包(rx_missed_errors、rx_no_buffer_count 上涨);环太大 → 缓存失效、延迟上升、内存占用增加。
ethtool -g eth0 # 当前与最大 ringethtool -G eth0 rx 4096 # 100G 建议 4096 起ethtool -S eth0 | grep -E 'missed|no_buffer|dropped'经验值:10G/25G 用 1024–2048,100G 用 4096,400G/800G 往往需要 4096 以上并配合更多队列。判断依据不是「调大就好」,而是丢包计数是否归零、以及延迟是否可接受。
同时要意识到:单个 ring 是串行消费的,一个队列一次只能由一个 NAPI 上下文收割。所以「单队列 ring 的处理速率」也是单流吞吐的天花板之一。
5.2 DMA 与 IOMMU:总线上的隐性开销
写合并(write combining)/ DMA coalescing:把多个小 DMA 写合成一次总线事务,减少 PCIe TLP 数量,这是网卡内部一项常被忽略的吞吐优化。 收发方向并不对称:收包是设备向内存 write,可以流水、乱序完成;发包是设备从内存 read,每个请求都要等一个 completion,受 outstanding read 数量与 MRRS 限制。所以同一个 x16 插槽上「收得动、发不动」是常见现象,瓶颈在 PCIe 的读并发度而不是标称带宽。 MPS / MRRS:Max Payload Size 与 Max Read Request Size 决定单个 TLP 能搬多少字节。BIOS 常把 MPS 保守设成 128 B; pcie_bus_perf会按父总线允许的最大值设 MPS,并把 MRRS 提到最大(Documentation/admin-guide/kernel-parameters.txt:5166-5170),对大包 DMA 吞吐有直接帮助。IOMMU:开启后每包都要做地址翻译/映射,小包场景开销可观。虚拟化必须开;裸金属追求极限吞吐时常用 iommu=pt(passthrough,设备走恒等映射)。既要隔离又要吞吐,可用iommu.strict=0(lazy 模式,延迟 TLB 失效换取吞吐,kernel-parameters.txt:2627-2633)——代价是设备隔离性下降。DMA 掩码:老驱动只声明 32 位 DMA 能力时会限制可用内存区间并增加 bounce buffer 拷贝,现代 64 位网卡不应出现。
5.3 巨帧:用「包更大」换「包更少」
MTU 从 1500 提到 9000,同样的数据量包数降到约 1/6,每包固定成本被摊薄近 6 倍——这是不改硬件、不改架构就能拿到的最大一笔吞吐收益。
ip link set eth0 mtu 9000ping -M do -s 8972 <peer> # 验证端到端真的支持代价必须一起算:
适用:存储网络、备份链路、集群内部东西向流量;不适用:公网出口、多租户混合流量、以低延迟小包为主的业务。
5.4 页面复用与零拷贝(结论)
page pool:把「每包一次 buddy 分配/释放」变成「从池里取、还回池」,同时缓存 DMA 地址,避免每包重新映射——100G 收包路径的必备项,见《page pool 设计原理与使用》。 发送侧零拷贝: sendfile()/splice()省掉用户态到内核的拷贝;MSG_ZEROCOPY省掉内核到网卡的拷贝(但需要处理 completion 通知)。接收侧零拷贝:AF_XDP 的 UMEM、devmem TCP 让网卡 DMA 直接落到用户态内存,见《内核网络零拷贝实现与 skb 设计》。
5.5 链路层流控:要么丢包,要么反压
队列满的时候只有两种结局:丢掉(让上层重传)或反压(让对端先别发)。这条选择题对有效吞吐的影响,比调 ring 大小大得多,却常被跳过。
ethtool -a eth0 # 看当前 pause 帧配置ethtool -A eth0 autoneg on rx on tx on # 开全局 pause802.3x pause(全局反压):缓冲区快满就发 pause 帧让对端停。代价是整条链路一起停(HOL 阻塞),一个慢接收方的影响会被放大到这条链路上的所有流。 PFC(802.1Qbb,按优先级反压):只暂停某个优先级,是无损网络(RoCEv2)的前提;配不好会带来 PFC 风暴和死锁(见 8.3)。 共享 buffer 与水线:100G+ 网卡把片上 buffer 按 pool 划分,xoff/xon 水线太紧会频繁 pause、太松就退化成 tail-drop。这部分不在 ethtool里,要看厂商工具或devlink sb(net/devlink/sb.c,devlink sb pool show/devlink sb occupancy show)。
经验规则:以 TCP 为主的通用流量宁可丢包也别开全局 pause(TCP 的拥塞控制比链路反压更懂全局);RDMA 必须靠 PFC + ECN 做到近无损;两者混跑要按优先级隔离,否则一条 RDMA 流就能把整链路的 TCP 流量一起按停。
六、把网卡变成「半个 CPU」:SR-IOV、硬件流表与 DPU
6.1 虚拟化:两条路径,性能差一个数量级
echo 4 > /sys/class/net/eth0/device/sriov_numvfs # 创建 4 个 VFSR-IOV 的本质是把队列和中断「切」给虚机,让 guest 绕过 host 协议栈和 vSwitch,性能接近裸金属。代价是可观测性(host 看不到 VF 内部流量)、迁移复杂度和 IOMMU 分组约束。折中方案是 vhost-user + DPDK:把 vSwitch 搬到用户态轮询,仍保留迁移能力。
注意一个常见误判:guest 里 ethtool -k 显示 TSO/GSO 全开,不代表真的卸载了。 virtio 的 offload 往往是「假卸载」——guest 侧省了,host 侧由 vhost 用软件补上,总成本没降。
6.2 硬件流表:让转发完全不进内核
tc filter add dev eth0 ingress protocol ip flower \ dst_ip 10.0.0.5 skip_sw action mirred egress redirect dev eth1skip_sw 的含义是「必须由硬件执行,硬件不支持就报错」——这一个参数决定了规则是真卸载还是假卸载。首包走慢路径建流,后续同流报文由网卡 eSwitch 直接转发,主机 CPU 归零。
收益:转发吞吐从「每包一次内核处理」直接到线速,且不占核; 代价:流表容量(几千到几万条)、首包延迟、规则变更的抖动、以及调试困难(包没进内核, tcpdump看不到)。
OVS 的 hardware offload 是同一机制的规模化版本,云场景常见。
6.3 SmartNIC / DPU:卸载的边界在哪里
DPU 的形态是「多核 ARM + 加速器 + 独立内存 + 独立 PCIe 通道」,典型卸载对象:vSwitch/OVS、IPsec/MACsec、TLS、NVMe-oF、压缩、正则匹配。
判断要不要上的算式很朴素:(被卸载的每包 CPU 成本 × pps)是否显著大于 DPU 的固定成本(采购、功耗、运维、可编程资源上限)。对 100G 小包转发、加密存储网关这类「每包成本高且量大」的场景成立;对普通 Web 服务往往不成立——把 RDMA 或简单卸载开好就够了。
七、绕开内核:RDMA、DPDK 与 AF_XDP
7.1 为什么必须绕
内核路径的固定成本是「系统调用 + skb + 分层解析 + 拷贝 + 调度」,量级在微秒;而线速小包允许的每包预算只有 100 ns 级。差了一个数量级,靠优化参数补不回来,只能换路径。
7.2 RDMA:让远端 CPU 完全不参与
形态:InfiniBand、RoCEv2(以太网承载)、iWARP; 机制:QP(队列对)+ CQ(完成队列),应用直接向网卡下发工作请求,网卡 DMA 读写已注册内存;支持单边 READ/WRITE——对端 CPU 不参与、不拷贝、不进协议栈;收益:100G/400G 线速而 CPU 接近空闲,延迟进入亚微秒级; 前提:RoCEv2 需要「无损或近无损」网络——PFC 防丢包 + ECN/DCQCN 防 incast,配置不当会在多打一时吞吐塌陷; 适用:AI 训练集合通信(AllReduce/AllToAll)、分布式存储、HPC; 不适用:连接数巨大、短连接的通用业务(QP 资源与连接模型不匹配)。
7.3 DPDK:把网卡交给用户态独占
PMD 轮询模式驱动 + 大页内存 + 独占核 + burst 收发(典型 32 个一批); 无中断、无系统调用、无协议栈,单核可做到 10–20 Mpps 量级; 代价:独占核(必须给 OS 留核)、没有内核协议栈(要么自研轻量栈要么只做转发)、运维与可观测性全靠自己。
7.4 AF_XDP / XDP:折中路线
XDP 在驱动最早点执行 eBPF 程序,可以 PASS/DROP/TX/REDIRECT;AF_XDP 让 XDP 把包重定向到用户态 UMEM,实现零拷贝到应用,同时保留内核的管理面(路由、防火墙、工具链)。
适合「比内核快、比 DPDK 好集成」的场景(如负载均衡、采集、轻量网关)。限制在于需要驱动支持 XDP native 模式与独立的 XDP 队列,且一个 XDP 队列仍是单核消费。
7.5 四条路线对比
八、横向扩展:聚合、多路径与拓扑
8.1 链路聚合救不了单流
ip link add bond0 type bond mode 802.3ad \ xmit_hash_policy layer3+4bond 的哈希按流分发到成员口,因此聚合带宽只对多流有效:两块 100G 做 bond,单条 TCP 连接仍然是 100G,永远到不了 200G。这一点和 RSS 是同一条物理规律在不同层的重复——流的保序需求把并行度锁死在一个执行单元上。
模式选择上,802.3ad(LACP)需要交换机配合但可检测链路故障;balance-rr 会把单流拆到多口,但会造成乱序,TCP 场景下反而降吞吐。
8.2 多路径与协议层聚合
MPIO(存储侧多路径)、NFS pNFS、MPTCP 都能做到「单连接跨多路径」,但都需要两端与中间设备同时支持;它们解决的是单流天花板,代价是复杂度和乱序/重排开销。
8.3 拓扑与收敛比:端到端线速的前提
收敛比:leaf-spine 1:1 无超订才能保证任意两点线速;3:1 超订在 incast 下必然排队。 ECMP:多路径负载均衡同样受「五元组哈希」限制,单流仍走单路径。 单机多卡:按 NUMA/rail 就近分配,避免跨 socket 转发;GPU 直连网卡的 rail-optimized 拓扑是 AI 集群标配。 无损域:PFC 配置要有边界,PFC 风暴和 HOL 阻塞会把局部拥塞放大成全域网吞吐塌陷。
九、怎么证明吞吐真的上去了
9.1 分层测量:从物理层往上查
ethtool eth0ethtool -S eth0 | rx_errors/crc_errors/frame_errors | |
/proc/interruptsethtool -S | rx_missed_errors/rx_no_buffer_count | |
/proc/net/softnet_statmpstat -P ALL 1 | %soft 是否单核打满 | |
ss -tinetstat -s | ||
iperf3 -c <ip> -P 8 -t 30 -w 4M --bidir |
softnet_stat 的列序值得记准(net/core/net-procfs.c:145-155):第 1 列 processed、第 2 列 dropped、第 3 列 time_squeeze、第 10 列 received_rps、第 11 列 flow_limit_count;7.1 还多出第 12~15 列——backlog 总长(input + process)、CPU 号、input 队列长度、process 队列长度。第 12 列是查「哪颗核积压了」最快的入口,比只看 dropped 有用。
第 2 列非零说明 backlog 溢出,通常是单核软中断处理不过来或 RPS 配置不均;第 3 列增长说明 NAPI budget 被耗尽——两者含义不同,别混。
9.2 六个最常见的假象
-P 8 多流;调 tcp_rmem/wmem 到 BDP 以上 | ||
lspci -vv 的 LnkSta,换插槽/平台 | ||
9.3 两个天花板公式
单流上限 ≈ min(链路速率, 窗口/RTT, 单队列处理能力, 单核处理能力) 多流上限 ≈ min(链路速率, 全部队列/全部核的处理能力, PCIe 单向带宽, 内存带宽)
遇到「跑不满」时,先判断卡在哪一项,再决定是调参(窗口、ring、队列)还是换架构(旁路、DPU)。
十、取舍矩阵:按场景组合技术
SO_REUSEPORT | |||
SO_BUSY_POLL、队列绑核 | iommu=pt | ||
落地 checklist(十二条)
先确认 Speed/Duplex和lspci的链路宽度,别在错误的物理前提上调软件。队列数设为参与处理的核数,队列与核、内存同 NUMA node。 RSS indirection table 与队列数对齐,隧道场景确认内层哈希已开。 开 adaptive-rx;吞吐场景再叠加rx-frames 64,仍压不住软中断就用gro_flush_timeout+napi_defer_hard_irqs(注意延迟会跟着走)。校验和、TSO/GSO、SG 全开;转发设备关 LRO。 100G 起把 ring 提到 4096,用 rx_missed_errors验证而不是凭感觉。内网存储/备份链路开巨帧,并做端到端 ping -M do验证。单流跑不满先算 BDP,再考虑多流或 MPTCP,别先怀疑网卡。 虚机性能问题先分清「真卸载」还是「vhost 兜底」。 上 DPU/RDMA 之前,先算「每包成本 × pps」这笔账,确认收益大于固定成本。 发送侧单独验一遍: tc qdisc是mq/mqprio(叶子队列带TCQ_F_NOLOCK)、xps_cpus按核配好;单流发送本来就只能用一个队列,别拿它当多核指标。流控策略要显式确认: ethtool -a核对 pause/PFC——TCP 通用流量不开全局 pause,RDMA 域必须 PFC + ECN,混跑时按优先级隔离。
结语:天花板在哪里
把四层天花板的量级并排放在一起,结论一目了然:
所以「提升网卡吞吐」这件事的正确答案从来不是某个开关,而是认清当前瓶颈在哪一层,然后用那一层对应的手段:链路层用巨帧和聚合减少包数与单点,主机层用多队列和 PCIe 代际匹配把并行度和带宽铺开,网卡内部用卸载和流表把每包成本搬走,软件层在「内核调优 → AF_XDP → DPDK → RDMA」这条谱系上按延迟与生态的取舍选点。
一旦某一层的量级差了一个数量级(典型是小包 + 100G 以上),调参就到此为止,必须换架构。
延伸阅读
外部标准与文档
IEEE 802.3df-2024(800 Gb/s 以太网标准,2024 年 2 月批准):https://www.ieee802.org/3/df/ IEEE P802.3dj(200/400/800 Gb/s 与 1.6 Tb/s,预计 2026 年发布):https://www.ieee802.org/3/minutes/nov23/2311_3dj_closed_report.pdf PCI-SIG PCIe 7.0 规范(2025 年 6 月发布,128 GT/s,x16 双向 512 GB/s):https://www.businesswire.com/news/home/20250611299049/en/PCI-SIG-Releases-PCIe-7.0-Specification-to-Support-the-Bandwidth-Demands-of-Artificial-Intelligence-at-128.0-GTs-Transfer-Rates man ethtool(-k/-K/-c/-C/-g/-G/-l/-L/-x/-X/-N/-n/-S的完整语义)Linux 内核文档 Documentation/networking/scaling.rst、netdev-FAQ.rstDPDK 程序员指南(PMD、hugepage、burst 模型)















































































