Scale-up/Scale-out下网卡与IO Die端点微架构拆解
2026/9/18 21:14:50 网站建设 项目流程

做服务器网络和系统架构这十几年,我被人问得最多的一类问题是:“交换机都 51.2T 了,网卡也 400G 了,链路为什么还是跑不满?”答案往往不在链路上,而在端点。这篇要聊的就是 Scale-up 与 Scale-out 两种扩展方式下,网卡本体和 IO Die 这两块微架构的设计逻辑——它们怎么把线速吃下来,又怎么在延迟和一致性上把自己逼到墙角。如果你在做服务器选型、存储/计算集群组网、RDMA 调优,或者单纯想在 400G/800G 时代不被“标称带宽”忽悠,这篇内容基本能覆盖你 80% 的疑问。剩下的 20%,是你自己机箱里那几块卡的 BIOS 设置和 NUMA 亲和性——那部分只能动手。

1. 先分清 Scale-up 和 Scale-out 分别在向端点要什么

1.1 两种扩展方式在网络语义上的差别

Scale-up 和 Scale-out 经常被当成“买更大的机器”和“买更多的机器”这种粗线条说法,但只要落到微架构层面,两者对网卡和 IO Die 的要求几乎是两个方向。Scale-up 关心的是域内语义:一个小规模的紧耦合域(通常一个机箱、一个刀片、一个超节点),die 与 die 之间用私有互连或类 CXL 的链路连起来,语义单元是 cache line,目标是让人看不出这是一堆芯片。它要的是内存语义 + 一致性 + 低延迟,能容忍的拥塞很小,一旦出现重传或者 cache line 乒乓,性能是断崖式下跌。

Scale-out 走的完全是另一条路。它的语义单元是消息,节点之间通过以太或 RDMA 组网,规模从几十到上万节点,拓扑是 Clos 或 Dragonfly,必须容忍丢包、必须做拥塞控制、必须能优雅降级。它要的是可预测的吞吐 + 相对宽松的延迟 + 可观测性。同一块物理网卡,在这两个场景下的微架构取舍是相反的:Scale-up 想要极少的队列、极低的单跳延迟、尽量少的状态;Scale-out 想要极多的队列(几千到几万个 flow 要隔离)、强大的分流和流控、丰富的计数器,以及能承受几十微秒抖动的缓冲。

维度Scale-up 域内Scale-out 节点间
语义单元cache line / load-store消息 / descriptor
典型延迟目标单跳 100~300 ns端到端 3~20 μs
一致性要求需要(目录协议)不需要(显式刷写)
传输载体私有 die 间链路、CXL、NVLink 类以太 / RoCE / IB
拥塞代价极高,几乎不允许排队可控,靠 ECN / DCQCN
端点角色片内网络的一个成员一个独立的协议翻译器

这张表最值得记住的是最后一行。它解释了为什么“端点才是最难的那一半”——在 Scale-out 里,网卡是一个边界设备,坏了换一块就行;在 Scale-up 里,网卡一旦被拉进一致性域,它就变成了片上网络的一部分,出错是整个域崩。

1.2 IO Die 的出场:端点从“外挂设备”变成了“片内成员”

Chiplet 流行起来之后,主流服务器处理器的布局基本都是“若干计算 Die + 一个大 IO Die”。这个 IO Die 承担的角色远不止“把线引出来”,它内部集成了 PCIe Root Complex、IOMMU/SMMU、内存控制器、die 间一致性引擎,有些设计里还带一部分末级缓存切片和内存侧的一致性目录。换句话说,网卡插在 PCIe 槽上,但它连接的其实是 IO Die,而不是“CPU”这个抽象概念

这件事的直接后果是:端点的微架构不再只是网卡自己的事。一块 NIC 从网线收到包,走 PHY、MAC、包处理、DMA,跨 PCIe 进入 IO Die,然后 IO Die 要决定这个写请求是命中 LLC、需要 snoop 远端计算 Die,还是直接落内存。每一个岔路口都有自己的延迟和带宽上限,而 NIC 侧的队列深度、突发特性会直接冲击 IO Die 的内部仲裁。我在实际项目里见过一个很典型的案例:两块 400G 卡插在同一条 PCIe Root Complex 下游,单卡测试都能跑到线速的 92%,双卡同时压测直接掉到 61%,原因不是 PCIe 带宽不够,而是两块卡的 DMA 突发在同一时间窗口内对 IO Die 的写通道形成了竞争,仲裁一抖动,NIC 侧描述符环回写就延迟,队列一深,端到端时延立刻涨上去。

1.3 为什么说端点是更难的那一半

把带宽做大这件事,在 fabric 侧其实是“线性”的:lane 不够就加 lane,波长不够就加波长,交换芯片 radix 不够就堆 chiplet。物理层每往前一代,工程难度确实涨,但方向和收益都是清楚的。端点难在它是收敛点:所有扩张的带宽最终要在几个有限的接口上被吸收——PCIe 通道、内存带宽、cache、DMA 引擎、中断队列、CPU 核。任何一环提前饱和,前面的带宽就是画在纸上的。

举个具体数字。400GbE 单向需要 50 GB/s 的持续搬运能力(400 Gbit/s ÷ 8)。PCIe Gen4 x16 的理论单向带宽是 31.5 GB/s,直接用一块 Gen4 卡接 400G 端口,即使不考虑任何协议开销,也已经超了 60%。这就是为什么 400G 网卡几乎清一色要求 Gen5 x16 起步,Gen5 x16 单向 63 GB/s,看起来有 26% 余量,但算上 TLP 头、DLLP 开销、描述符回写和内存读回的正反向流量,真实可用余量往往不到 10%。再往上走 800GbE,单向 100 GB/s,Gen5 x16 直接不够,要么走 Gen6 x16,要么把网卡逻辑塞进 IO Die 里做片内直连——这就回到了标题里那个判断:端点侧的物理通道,比 fabric 更早撞墙。

2. 网卡微架构拆解:一个包在端点里经历了什么

2.1 内部功能分区的真实划分

现代高性能网卡内部大致可以切成五块:光/电接口与 SerDes、MAC/PCS 与包解析、包处理器(含表项查找与修改)、DMA 与主机接口、以及一块用来缓存描述符和包的片上 SRAM。这个划分听起来平淡,但每一块的资源配比决定了卡的性格。

SerDes 决定端口速率和误码率,这块基本是买 IP 的活。MAC/PCS 负责帧同步、CRC 校验、流控,通常会把 pause 帧的处理放在最靠前的位置,因为流控必须比包处理快。包处理器的核心是表项查找宽度和流水线级数:查找表塞得越深,支持的规则越复杂,但流水线越长、延迟越高。这块是各家差异最大的地方,也是“同样是 400G,为什么一块卡转发延迟 400 ns、另一块要 900 ns”的主要来源。

DMA 和主机接口是最容易被忽略、但实际上最决定成败的一块。它不是一个简单的搬运工,它要处理描述符环的预取、doorbell 的合并、写回的顺序、以及跨 PCIe 的事务打包。片上 SRAM 的容量决定了能挂多深的队列、能缓存多少 in-flight 描述符。这块要是小了,高并发场景下你会看到队列利用率上不去、CPU 侧空转。

2.2 队列与 DMA:描述符环、doorbell 与预取策略

我一般跟人解释 DMA 时用快递柜的类比:描述符环就是那一排柜子,每个柜子里写着“货送到哪个地址、多长”;doorbell 就是你按的那个取件通知;而网卡内部的预取引擎,就是提前把几个柜子打开、把标签读进手里,避免每送一件货就回去看一次柜子。

这套机制里有两个坑,几乎所有新手都会踩。第一是描述符环的对齐与大小。队列环如果跨了 cache line 没有对齐,每次更新 head/tail 指针都要多一次无效化,高包率下这部分开销能吃掉 10% 以上的 CPU。第二是doorbell 频率。每收一个包就敲一次 doorbell,PCIe 上会塞满小尺寸写事务,效率极低;批量敲又会让延迟变高。实践中常见做法是设置一个阈值(比如攒够 8 个描述符或者超时 10 μs 就敲一次),这个阈值没有通用最优解,必须配合你的包长分布实测。

具体到参数上,我会先看几个东西。ethtool -g看环形缓冲,ethtool -l看队列数,ethtool -c看中断合并。这三个值构成了一条非常直接的因果链:环深决定能扛多少突发,队列数决定能分到多少 CPU 核,合并参数决定中断频率和延迟的平衡点。很多人调优只改其中一个,效果当然不明显。

# 先看现状 ethtool -g eth0 # Ring buffer ethtool -l eth0 # Channels / 队列数 ethtool -c eth0 # Coalesce 参数 ethtool -k eth0 # 卸载开关 cat /sys/class/net/eth0/device/numa_node # 这张卡挂在哪个 NUMA 节点

2.3 中断与轮询:MSI-X、NAPI 与纯轮询的取舍

MSI-X 的中断向量数量决定了你能把中断散到多少个核上。一块现代网卡给 64 到 512 个向量都很正常,关键不是数量本身,而是向量到队列到 CPU 核的映射是不是一对一且绑定在正确的 NUMA 节点上。我见过太多机器是这样的状态:网卡在 node 0,中断却被 irqbalance 全甩到 node 1,跨 die 中断往返一次多出 60 到 100 ns,包率一高,收包核全在等中断。

中断合并的工作模式值得单独说一下。NAPI 的设计是“第一个包来中断,之后转为轮询”,这个设计的原文表述很朴素,但它隐含了一个前提:包到达速率足够高,轮询一次能拿到多个包。如果你的业务是稀疏流量(比如每秒几千个包的 RPC 长连接),NAPI 的轮询预算反而会让每个包多花几百纳秒去空转。这时候要么开中断合并让中断更少,要么干脆用 busy-poll,代价是烧 CPU。

纯轮询模式(DPDK、部分用户态协议栈)把中断完全去掉,收包核 100% 时间在转,换来的是最稳定的延迟和最高的包率。代价非常直白:每个收包核都要独占一个物理核,还得考虑 C-state 和频率缩放——我在一台机器上踩过这个坑,BIOS 里 C-state 没关,收包核频繁进出 C1,P99 延迟直接劣化到原来的三倍。这类问题不靠读手册是发现不了的,只能靠turbostat加压测对比。

2.4 卸载能力的分界:哪些该下放,哪些必须留软件

卸载是把双刃剑。我列一张表,按我自己的经验给出适用边界,你对着业务形态看就行。

卸载能力该下放的场景建议留在软件的场景
校验和计算几乎所有场景
TSO / LRO大包为主的批量传输小包、低延迟敏感、需要精确时延分析
VLAN / 隧道封装大流量隧道网关需要复杂策略、动态隧道的场景
RDMA / RoCE存储、HPC、AI 集合通信有大量小消息且需要灵活分片时慎用
TLS / IPsec 卸载固定密钥、长连接的加解密会话切换频繁、密钥轮换密集
时间戳(PTP)金融、测量类业务对时钟精度无所谓的一般业务

这张表的核心逻辑是:卸载适合“行为模式稳定”的流量,不适合“决策频繁变化”的流量。因为卸载逻辑一旦下放到硬件,它的表项更新要走控制面,路径长、频率低。一个每秒新建几万个连接的网关,把隧道封装全下放到硬件,表项同步都能把 CPU 吃满。

3. IO Die 微架构:端点为什么在 Chiplet 时代彻底变了

3.1 IO Die 里到底放了什么

一颗典型的服务器级 IO Die,内部至少包含这几样:PCIe Root Complex 及其对应的几个 x16 控制器、IOMMU/SMMU、内存控制器与 PHY、die 间互连的物理层与链路层、一致性目录的一部分、以及作为末级缓存代理的接口。这些单元共享 IO Die 内部的片上网络,而这块片上网络的带宽和仲裁策略,就是端点性能的天花板。

理解这一点的意义在于:PCIe 端点的带宽不是“总线带宽”,而是“共享片上网络的分配结果”。我今天实测过一个现象,在同一台机器上把 GPU、NVMe 和网卡都挂到同一颗 IO Die 下,NVMe 做顺序大块写的时候,网卡的 P99 收包延迟会涨 40% 左右。不是 PCIe 抢不过,是 IO Die 内部的写通道仲裁对突发更敏感,而 NVMe 的写突发远比网卡更“贪”。

所以选型时我会先看拓扑:几颗 IO Die、每颗下面挂什么、die 间链路是几条、对称不对称。这些信息在lspci -tvlstopo(hwloc 提供)里都能看出来,我建议每个人都至少跑一次,对自己机器的物理拓扑形成肌肉记忆。

lspci -tv # PCIe 树状拓扑,看设备挂在哪个 root complex 下 lstopo --of console # 完整的 NUMA / PCI / 缓存拓扑 numactl -H # 节点与距离矩阵

3.2 一条包路径的延迟构成:把数字摊开看

我习惯把端到端延迟拆成五段来看,任何一段膨胀都能定位到具体环节。

环节典型延迟影响因素
SerDes / PHY20~60 ns速率、编码、FEC 开关
MAC + 包处理30~120 ns流水线级数、表项深度
PCIe 控制器与协议层50~120 nsGen 代数、TLP 大小、ASPM
IO Die 片内路由30~80 ns跨 die 与否、仲裁竞争
内存子系统80~160 nsLLC 命中率、DDR 代际、跨 socket

把最理想的情况加起来,纯硬件的单向延迟在 250 到 500 ns 之间,这是物理下限。再往上叠加软件栈:内核网络栈一次收发通常在 3 到 8 μs,DPDK 用户态在 600 ns 到 2 μs,RDMA 单边操作在 1 到 2 μs。这些数字之间的差距,就是“为什么同样的硬件,有人能做到 1.5 μs RTT,有人只能做到 15 μs”的全部原因。

还有一笔账经常被忽略:串行化延迟。一个 1500 字节的帧在线速 100GbE 上需要 12000 bit ÷ 100 Gbit/s = 120 ns 才能“推”上线;换成 64 字节小包,只有约 6.7 ns。这意味着小包场景下,延迟几乎完全由处理路径决定,跟带宽没关系。反过来,大包场景里串行化延迟会迅速变成主导项,这时候你优化队列、优化中断,收效都很有限。

3.3 带宽账:三个接口之间的“剪刀差”

端点侧的带宽必须同时满足三个不等式,缺一个都会成为瓶颈。我拿 400GbE 举个完整算例。

第一是端口侧:400 Gbit/s ÷ 8 = 50 GB/s 单向,双向就是 100 GB/s 的总搬运量。

第二是 PCIe 侧:Gen5 单 lane 有效速率 32 GT/s × 128/130 = 31.5 Gbit/s ≈ 3.94 GB/s,x16 就是 63 GB/s 单向。看起来够,但要注意 PCIe 的读和写共享这一组 lane,收包(设备写内存)和发包(设备读内存)在双向满载时会互相挤占,另外描述符回写、完成报文(Completion)也占带宽。经验上可用效率打七折,实际单向能力约 44 GB/s,刚好卡在 50 GB/s 需求线下方——这就是为什么真正的 400G 双口卡会要求 Gen5 x16 并且强烈建议 BIOS 里关掉 ASPM。

第三是内存侧:网卡 DMA 写内存会消耗内存带宽。DDR5 单通道大约 38.4 GB/s(4800 MT/s × 8 Byte),一个 8 通道系统理论 307 GB/s,看上去很宽,但 CPU 同时还要跑业务。我一般的经验值是:给网卡留出的内存带宽不超过总量的 20%,超过之后业务侧的性能抖动会明显变大。按这个口径,307 GB/s × 20% = 61 GB/s,勉强够 400G 单向,双口 400G 就需要额外的内存通道或者更高效的写合并策略。

端口速率单向需求PCIe 要求(含冗余)内存侧建议余量
100GbE12.5 GB/sGen4 x8 起≥ 30 GB/s
200GbE25 GB/sGen4 x16≥ 60 GB/s
400GbE50 GB/sGen5 x16≥ 120 GB/s
800GbE100 GB/sGen6 x16 或片内直连≥ 240 GB/s

3.4 NUMA 与亲和性:端点的“位置”比“数量”更重要

在 IO Die 架构下,一块网卡是严格挂在某一颗 IO Die 上的,它天然属于某个 NUMA 节点。你在这张卡上收包,数据落到内存,如果处理这个包的 CPU 核在另一个节点,那么每一次访问都要跨 die,延迟多 60 到 100 ns,带宽还要打对折。

我做过一组对比测试,同一块 200G 卡、同样的包率,绑定在同一节点和跨节点的结果差得离谱:同节点时 P99 收包延迟 8.2 μs,跨节点直接到 21 μs,而且 CPU 利用率更高——多出来的部分全花在跨节点缓存同步上。这个差距不是调参能补回来的,只能靠拓扑对齐。

做法很简单,三步就够:先确认卡的 NUMA 节点,再确认要用的 CPU 核属于哪个节点,然后把中断亲和性写死。别指望 irqbalance 的默认策略,它在多卡多队列场景下的判断经常是反的。

# 1. 卡在哪个节点 cat /sys/class/net/eth0/device/numa_node # 2. 该节点有哪些核 lscpu | grep NUMA cat /sys/devices/system/node/node0/cpulist # 3. 把某个队列的中断绑到指定核(示例:IRQ 128 绑到 node0 的 8-15 核) echo 00ff > /proc/irq/128/smp_affinity cat /proc/irq/128/smp_affinity_list

4. 实操:把端点侧的带宽与延迟算清楚并压出来

4.1 第一步:确认 PCIe 链路真的跑在了协商速率上

这一步看起来低级,但它是排查顺序里的第一位。我遇到过至少三次“性能不对劲”,最后都定位到 PCIe 链路降速:一次是转接卡布线问题把 Gen5 降到了 Gen3,一次是 BIOS 里某个功耗策略把链路压到了 Gen1,还有一次是插槽物理上只支持 x8,而所有人都以为它是 x16。

# 看当前协商的速率和宽度 lspci -vv -s 03:00.0 | grep -E "LnkCap|LnkSta"

输出里要盯三个字段。LnkCap是链路能力(硬件上限),LnkSta是当前状态,还有LnkSta后面括号里的降级原因。如果看到Speed 16GT/s (downgraded)或者宽度显示Width x8 (ok),那就要分头查:是硬件不支持,是卡没插牢,还是 BIOS 里 PCIe 的速率策略设成了自动降速。

还有一个容易漏的点:某些平台的 IO Die 上,不同 PCIe 控制器能跑的最高代际不一样,靠近内存控制器的那个控制器可能支持 Gen5 x16,靠外的只支持 Gen4 x16。这种事只有主板手册和lspci的输出一起看才能确认,标称“支持 Gen5”和“每个槽都跑 Gen5”是两回事。

4.2 第二步:把队列、中断、内存三者的亲和性对齐

这一步的目标很明确:让一个包从网卡进来到被 CPU 处理完,全程不跨 NUMA 节点。实现它需要三个绑定同时成立。

第一是队列数与 CPU 核数的匹配。多队列网卡通常按 RSS 把流量哈希到不同队列,队列数一般设成可用收包核数的整数倍。注意队列数不是越多越好,队列太多会导致每个队列的环变浅、中断向量紧张、cache 局部性变差。我的经验值是每个核一到两个队列,超过之后收益递减很快。

第二是中断亲和性。上面给的smp_affinity写法是最直接的,但要注意 mask 是十六进制位图。核数多的时候手算很容易错,用smp_affinity_list更安全,直接填核号列表。

第三是内存分配。收包路径上的缓冲区、描述符环这些内存,必须来自网卡所属节点的本地内存。用 DPDK 的话,--socket-mem参数要按节点分别给;用内核栈的话,靠的是进程的 NUMA 策略和numactl --cpunodebind --membind

# 一次性把某块卡的所有队列中断都绑到指定节点 NODE=0 CPUS=$(cat /sys/devices/system/node/node${NODE}/cpulist) for irq in $(grep eth0 /proc/interrupts | awk -F: '{print $1}'); do echo $CPUS > /proc/irq/${irq}/smp_affinity_list done

注意:smp_affinity_list接受的是如8-158,9,10这样的格式,直接填 CPU list 的原始字符串通常就能用,不需要自己转成位图。填完一定要回头cat一遍确认。

4.3 第三步:压测矩阵——不同包长、不同流数、不同方向

单跑一个iperf3得出“能跑满”的结论是没有意义的。端点侧的性能必须用一个矩阵来描述,因为它的瓶颈项会随着包长和并发流数变化。

我常用的矩阵是三个维度:包长(64B / 128B / 512B / 1500B / 9000B),流数(1 / 8 / 64 / 512),方向(单向 / 双向)。每个组合记录三件事:吞吐、平均延迟、P99 延迟。这个矩阵跑下来通常要一两个小时,但它能立刻告诉你瓶颈在哪。比如小包单流跑不满,那基本是包率瓶颈,看的是 PPS 而不是带宽;大包双向跑不满,看的是 PCIe 或内存带宽;多流小包 P99 暴涨,看的是队列分配和中断亲和性。

工具上,带宽用iperf3 -Pib_write_bw,包率用sockperf throughput,延迟用sockperf ping-pongib_write_lat。要注意iperf3默认会开很大的 socket buffer,测出来的延迟偏乐观,我一般会额外跑一轮关闭 buffer 自动调整的版本。

# 带宽:多流 iperf3 -c 10.0.0.2 -t 30 -P 8 -l 1500 # 延迟:ping-pong 模式 sockperf ping-pong -i 10.0.0.2 -t 30 --mps=max -m 64 # RDMA 侧 ib_write_bw -d mlx5_0 -s 65536 -q 16 -a ib_write_lat -d mlx5_0 -s 2 -a

4.4 第四步:观测与判读——哪些计数器值得盯着看

压测跑起来之后,真正有价值的信息在计数器里。ethtool -S输出的动辄几百行,但常用的就那么几个,我把它们整理成一张速查表。

计数器含义涨了说明什么
rx_missed_errors收到的包因为没有描述符被丢环太浅或 CPU 处理不过来
rx_nombuf/out_of_buffer缓冲区分配失败内存带宽紧张或分配策略有问题
rx_fifo_errors片上 FIFO 溢出包处理流水线跟不上线速
tx_carrier_errors物理层链路问题光模块、线缆、对端端口
pause_frames_xoff收到对端流控对端端点被压住了,问题在对面
rx_discards_phyPHY 层丢弃FEC 纠错失败或误码率偏高

我判断问题的顺序是:先看丢包类计数器,再看流控类,最后看错误类。因为丢包是性能问题,流控是竞争问题,错误是硬件问题,三者的排查路径完全不同。

配合perf还能看到更细的东西,比如 LLC miss 率和跨节点的远程内存访问比例:

perf stat -e cache-misses,LLC-load-misses,LLC-store-misses,node-load-misses -a sleep 10 numastat -p $(pgrep myapp) # 进程内存的节点分布

4.5 BIOS 与内核侧的几个关键开关

软件层面能做的事情有限,很多时候你必须回到 BIOS。这几个我基本是默认必查的:ASPM 关掉(省那点功率换来的是延迟上限);Above 4G Decoding 打开(否则大 BAR 分配不到,多队列和大环都用不起来);C-state 限制到 C1 或者直接关闭(延迟敏感场景);PCIe 速率策略设成固定代际,不要自动协商;NUMA 保持开启,别为了“看起来一致”把它关掉,那只会把所有问题藏起来。

内核命令行里,pcie_ports=native让内核直接管 PCIe,iommu=pt让直通设备跳过地址翻译开销,intel_iommu=on或者对应的 AMD 选项在需要隔离时必须开。这几个参数组合起来,能明显改善大页和 DMA 场景下的表现。

5. 常见问题与排查实录

5.1 常见问题速查表

现象最可能的原因先查什么
单口能满,双口一起跑就掉IO Die 仲裁或内存带宽竞争两块卡是否在同一 Root Complex、numastat
小包包率上不去,CPU 却不高中断亲和不均或队列太少/proc/interrupts分布、ethtool -l
大包吞吐正常,小包延迟抖动大C-state、中断合并、ASPMBIOS 设置、ethtool -c
RDMA 带宽只有预期一半队列对数量、MTU、PCIe 降速ibv_devinfolspci -vv
跨节点流量性能差一倍亲和性没对齐numa_node、进程绑核绑内存
压测一段时间后突然掉速温度触发降频或流控传感器数据、pause计数器
虚拟机里网卡性能差SR-IOV 未启用或 VF 队列太少lspci看是否 VF、ethtool -l

5.2 几个容易误判的场景

第一个误判是把丢包当成带宽不足rx_missed_errors涨了,很多人第一反应是“加带宽”,其实绝大多数情况是收包核处理不过来导致描述符耗尽。正确做法是先看收包核的%soft占用,再决定是加核、加深环,还是降低每包开销。

第二个误判是把延迟抖动归咎于网卡。我碰到过一次 P99 从 5 μs 跳到 300 μs 的情况,查了两天网卡固件,最后发现是同机器上的另一块卡在做大规模 DMA,触发了 IO Die 的写通道仲裁抖动。这类问题的特征是:抖动和业务流量不相关,但和机器上其他设备的活动强相关。排查方法是把其他设备逐个下线,看抖动是否消失。

第三个误判是忽略虚拟化层的开销。SR-IOV 的 VF 虽然直通,但 VF 的队列数、中断向量数通常远少于 PF,Hypervisor 的 IOMMU 映射也会带来额外延迟。如果你在虚机里测出来的延迟比物理机高 3 到 5 μs,这属于正常范围,别往网卡上找原因。

5.3 踩过的坑和一些不太写在文档里的经验

第一,别迷信网卡的标称队列数。规格书写的 1024 队列,实际可用往往受限于中断向量数、MSI-X 表大小和驱动实现,尤其在某些联合驱动场景下,真正能开起来的可能只有几百个。选型时我会直接问厂商“在什么内核版本、什么固件版本下能实际开满”,这个答案比规格表有用。

第二,BIOS 升级会改拓扑。有一次客户升级 BIOS 之后,网卡对应的 IO Die 编号变了,所有绑核脚本失效,性能掉了一半。我的建议是把自己的 NUMA 拓扑信息写进部署脚本的校验环节,每次上线前对一遍。

第三,温度对端点的隐形影响。高速 SerDes 对温度很敏感,机箱风道不好时,长时间压测会让误码率上升,触发 FEC 纠错甚至重传,表现出来就是“跑了一段时间之后吞吐慢慢掉”。这种情况在计数器里能看到rx_discards_phy缓慢增长,别去查软件,去查风道。

第四,描述符环的预取策略要配合业务。我做过一组对比,把环深从 512 提到 4096,小包场景的 P99 延迟反而变差了。原因是环太深,预取引擎会一次性拉很多描述符进来,cache 占用上升,反而挤压了真正需要的热数据。环深不是越大越好,它是和包率、包长、cache 容量一起算出来的

第五个小经验是关于测试方法本身的。很多人用单线程工具测完就下结论,但端点侧的性能是并行系统,单流测出来的数字基本只反映单核能力。我一般会先用单流建立基线,再用多流看扩展性曲线,当曲线在某个流数之后开始走平,那个点就是端点的真实并发上限。

第六个是关于多代际混插的。老卡和新卡在同一个 IO Die 下混用,往往拉低整体性能,因为不同卡的 MTU、流控策略、中断频率都不一样,共享通道时仲裁会更频繁。如果是关键业务,我建议同一下游只放同代际同型号的卡。

5.4 一个完整的排查案例:从 400G 跑不满到定位到 IO Die

最后讲一个完整的案例,因为它几乎把上面所有环节串起来了。

现象:一块 400G 双口卡,单口压测能到线速的 88%,双口同时压测掉到单口的 55%,也就是整体只有 48% 左右。CPU 利用率不高,没有明显丢包。

第一步查链路。lspci -vv显示 Gen5 x16,速率正常,宽度正常,没有降级。排除链路问题。

第二步查内存带宽。numastat显示进程内存全在 node 0,卡的numa_node是 0,看起来对齐了。但用perf stat看 node-load-misses,发现远程访问比例有 18%,说明有一部分流量其实在跨节点。进一步查发现,中断亲和性虽然设置了,但irqbalance服务在运行,会定时把设置覆盖掉。停掉服务、重新绑核之后,远程访问降到 2% 以下。

第三步查 IO Die 竞争。两块卡分别在两个不同的 PCIe 控制器上,但这两个控制器属于同一颗 IO Die。用lspci -tv确认拓扑后,把两块卡的中断分散到不同 CPU 核,并调整了中断合并参数,让两块卡的中断不要在时间上重叠。

第四步调环深。把描述符环从默认的 1024 提到 2048,tx侧同时提高。双口并发时丢包消失。

最后结果是双口并发能到单口的 92%,整体利用率 81%。剩下的 19% 里,一部分是 PCIe 和 IO Die 仲裁的固有开销,一部分是内存带宽的物理上限。到这一步就没法再优化了,属于架构决定的边界——这也是我一开始说“端点是更难的那一半”的原因:算法能把网络侧推到 95%,但端点侧的最后 15%,你要跟硅片商量。

我个人在实际项目里的体会是,端点调优最忌讳的就是“只改一个参数看效果”。网卡、PCIe、IO Die、内存、NUMA、BIOS 这几层是强耦合的,一个参数的变化往往会通过仲裁和亲和性传到其他层。所以我现在做任何调优,都是先把拓扑和基线完整记录下来,改一个变量、做一次矩阵压测、存一份计数器快照。这套流程慢,但它能保证你最后拿到的不是一个偶然跑出来的漂亮数字,而是一个可复现、可解释、别人接手也能看懂的结果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询