刚接手一台新服务器时我习惯先看一眼top和/proc/interrupts。很多人不明白为什么要对一个“网卡调度”这么上心。我举个例子同样的千兆带宽默认配置下可能跑满 500Mbps 时 CPU 就飙到 80%软中断softirq全压在一个核上业务延迟忽高忽低而调过中断亲和性和多队列参数之后同样是这台机器跑到 900Mbps 依然游刃有余。网卡调度不是玄学它本质上解决的是“网络数据从网卡到应用程序之间由谁、在哪个 CPU 上、以什么顺序处理”的问题。这篇文章我会从实际运维和排查经验出发把这些关键点拆开来讲适合刚接触 Linux 网络性能调优的工程师也适合已经踩过坑、想系统地理解链路原理的人。Linux 网卡调度1. 先搞清楚网卡调度到底在管什么1.1 从收包第一步说起如果你在裸机上抓过包或者用tcpdump观察过高速流量很快就会遇到一个问题数据包到了网卡网卡有没有直接把它“喂”给应用程序答案是没有。数据包从物理线缆进入网卡在网卡内存中先被暂存然后通过 DMADirect Memory Access写入内核分配好的内存缓冲区这个过程不占用 CPU。之后网卡会发出一个中断信号通知 CPU“有数据来了请处理”。中断触发之后才是“调度”真正的开始。内核收到中断信号后要决定由哪一个 CPU 核心来处理这个中断。如果所有中断都落到 Core 0那么所有网络栈处理都堆在这个核上。你可以在top里按1看每个 CPU 的负载就会看到那一个核的si软中断占用特别高其余核却很闲。这就是经典的“中断不均”问题。这里的核心概念值得展开说一下硬中断hardirq是网卡硬件发出来的CPU 必须立刻响应软中断softirq是内核在硬中断之后安排的延后处理流程真正的协议栈解析、数据分发就是在软中断里做的。Linux 内核通过net_rx_action这个函数把驱动收到的数据包从队列里取出来一层层送进 IP 层、TCP/UDP 层。网卡调度核心就是管理这些硬中断和软中断在不同 CPU 上的分配以及驱动程序向内核提交数据包的节奏。还有一个容易误会的点很多人以为数据包被中断之后就“直达”应用进程了中途没有别的调度。实际上内核还涉及“接收队列”和“处理顺序”的调度。不同进程绑定在哪个 CPU以及网络数据最终送到哪个 socket 队列会受到 RPSReceive Packet Steering机制的影响。简单说从包进网卡到 enter 到用户态 socket整个链路里有两到三处可以“分流”的地方每一处都对应不同的调度手段。用生活中的类比来理解网卡好比快递总站数据包是快递。中断就是快递到了之后总站前台喊了一嗓子“来活了”。如果只有一个前台一个 CPU 核心接单所有快递搬运、分拣、派发都得走这一个窗口其他窗口闲得无聊那吞吐自然受限。多队列、多核调度本质就是多开几个前台同时按规则分快递让每个窗口干自己该干的活。1.2 中断 vs 轮询NAPI 的历史意义早期网卡驱动处理数据包的方式是“一个包一次中断”。当网络速率较低时这种方式没有太大问题但当网卡达到千兆甚至更高速率时这个模式就变成了灾难——每来一个包就打断 CPU 一次高频中断让 CPU 忙于上下文切换几乎没时间干正事。Linux 内核后来引入了 NAPINew API核心思路是高负载下从“中断驱动”切换到“轮询驱动”。NAPI 的工作方式可以用一句话概括网卡收到第一个包时依然会发中断但 CPU 进入中断处理流程后会立刻关闭该网卡后续的 RX 中断然后以轮询方式从网卡队列里批量取包处理直到队列清空或者处理配额用完才重新开启中断。这个机制背后是对硬中断数量的大幅压缩——把“来一个包处理一次”变成“来一批包集中处理一次”。这个设计对“网卡调度”的意义太大了。没有 NAPI谈多队列、谈软中断均衡都是空谈因为 CPU 光服务硬中断就忙不过来了。有了 NAPICPU 才有余量去精细地做负载分配。所以当你调优网卡性能时首先得确认自己的驱动是不是基于 NAPI。比如ixgbe、i40e、mlx4_en这些主流驱动都支持默认就是开着的不需要额外配置。倒是某些低端虚拟网卡或者老旧的驱动还在用传统中断模式性能天花板很明显。NAPI 有一个参数值得留意net.core.netdev_budget。它表示一次轮询周期里CPU 最多处理多少个数据包。默认值是 300这意味着如果网卡队列里堆积了大量包一次 softirq 最多处理 300 个就暂时退出等待下一个时钟周期再次调度。对于万兆网卡、高频小包场景300 可能不够用实际测试中我一般会把它调到 600 到 1000。另一个相关参数是netdev_budget_usecs它限制的是处理时长微秒防止某次 softirq 占用 CPU 过久。这两个参数合在一起控制着“轮询节奏”。$ cat /proc/sys/net/core/netdev_budget 300 $ cat /proc/sys/net/core/netdev_budget_usecs 2000需要泼一盆冷水的是这两个参数不是无脑调大就好的。netdev_budget调太大一次软中断处理时间过长会带来两个问题一个是网络延迟敏感型应用比如实时音视频可能出现抖动另一个是单次 softirq 时间过长可能触发内核调度延迟影响其他任务。调优的原则是“够用就行”不要为了追求峰值吞吐而牺牲平均延迟尤其在业务没有明显丢包或 CPU 瓶颈时不要动它。2. 核心手段一中断与 CPU 亲和性2.1 看懂 /proc/interrupts 的中断分布在 Linux 下诊断网卡调度问题第一步永远是看中断分布。执行以下命令watch -n 1 -d cat /proc/interrupts这个文件会按 CPU 核心列出每个中断号的累计触发次数。你需要先找到网卡对应的中断号。用ethtool -i eth0可以查看驱动和总线信息再用cat /proc/interrupts对照网卡名称来找。常见命名是eth0-TxRx-0、eth0-TxRx-1这样的格式后面的数字就是队列编号。正常情况下支持多队列如 RSS 的网卡每个队列都会有一个独立的中断号。你会看到这些中断累加值在不同 CPU 之间分散分布。如果看到几十个中断号全部堆积在 CPU 0说明亲和性配置失效了或者该网卡驱动不支持多队列中断分配。CPU 列表那一列从左到右对应 CPU0、CPU1、CPU2……观察累计值增长最快的在哪一列就能定位“软中断热点核心”。在top里按1看si列的百分比可以交叉验证。就我的经验/proc/interrupts里增长速率不均是排查网卡调度问题最直观、最可靠的入口没有之一。很多花里胡哨的监控面板数据源头也是从这些 proc 文件扒出来的。2.2 实战配置 smp_affinity当你确认中断分布不均时手动设置中断亲和性就是最直接的办法。每个中断号在/proc/irq/中断号/smp_affinity里都有一个掩码文件。比如你要把中断号 78 绑到 CPU 0 和 CPU 1可以这样操作echo 3 /proc/irq/78/smp_affinity echo 3 /proc/irq/78/smp_affinity_listecho 3对应二进制11意思是 CPU0 和 CPU1。smp_affinity是十六进制掩码smp_affinity_list的内容是 CPU 编号列表后者的可读性更好。改完之后用cat /proc/irq/78/smp_affinity_list确认是否生效。这里有个细节容易踩坑不要只设置高于 15 的 CPU 编号而不考虑十六进制换算。16 核以上的机器掩码需要按组换算。比如 CPU16 对应十六进制第 5 位CPU0-15 对应的掩码低 16 位是ffff如果要把 CPU0 到 CPU16 全绑上就应该写成echo 1ffff /proc/irq/78/smp_affinity为了让中断亲和性在重启后依然生效你需要把配置写进持久化脚本。常用的做法是放到/etc/rc.local或者利用 systemd 服务在开机后执行。也有第三方工具irqbalance和tuna可以辅助但手工配置的方式最稳定可控。真正生产环境里手工配置亲和性要遵循一个原则物理网卡的多个队列尽量分散到同一个 NUMA node 对应的 CPU 上。如果网卡插在 Node 0 的 PCIe 插槽上那么把它的中断绑定到 Node 0 的 CPU 上可以避免跨 NUMA 访问内存。判断的方法是用lstopo或者lscpu -p查看 CPU 拓扑。若不这样做中断处理要在远端 CPU 上访问网卡 DMA 的内存描述符延迟和带宽都会受影响。2.3 要不要开 irqbalance在很多 Linux 发行版里irqbalance 默认是开启的。这个服务的初衷是每个一段时间自动重新平衡系统里的中断分布避免中断长时间集中在单个 CPU 上。它的优点是完全自动、免维护缺点是在某些高吞吐、要求极低延迟的场景irqbalance 的调整逻辑是基于“系统整体负载”的粗粒度判断可能把中断从一个 CPU 挪到另一个 CPU造成瞬时性能抖动。所以我的建议很明确如果是低负载的通用服务器开着 irqbalance 没毛病但如果你是跑高负载网络服务的机器比如负载均衡器、网关代理、高性能数据库建议关掉它手工分配亲和性。关闭方法也很简单systemctl stop irqbalance systemctl disable irqbalance关掉之后你只是失去了“自动平衡”但换来的是“确定性”——每个网卡队列的中断固定落在某个 CPU 上不会突然搬家。这个确定性的价值在高并发场景下非常大CPU 缓存命中率更高软中断调度更稳定热点可见、可预测出问题时能迅速定位。另外一个值得提到的工具是tuna它是红帽开发的中断/CPU 调优工具。用它可以快速地把一阵列表中的中断绑定到一组 CPU 上。如果说手工写掩码太痛苦、irqbalance 又太激进tuna 是中间路线。不过在生产环境里我更鼓励你自己理解掩码逻辑因为只有当你明白亲和性的“为什么”才能在别人教你的脚本失效时自己解决问题。3. 核心手段二多队列与 RSS/RPS/RFS3.1 多队列网卡怎么看出有几个队列现代网卡基本都支持多队列。所谓多队列就是网卡内部有多组 DMA 描述符每组描述符关联一个 RX 队列而每个 RX 队列可以拥有独立的中断号。这样多个 CPU 就可以并行从不同队列取包彻底打破单队列时代“一个中断号一个 CPU 处理所有数据包”的瓶颈。查看网卡队列数量最简单的方式是执行ethtool -l eth0$ ethtool -l eth0 Channel parameters for eth0: Pre-set maximums: RX: 0 TX: 0 Other: 1 Combined: 8 Current hardware settings: RX: 0 TX: 0 Other: 1 Combined: 8这里的Combined: 8表示当前网卡收发共有 8 个队列。如果显示RX: 0、TX: 0通常说明驱动不支持独立的收发通道只能用 Combined 模式。你可以执行ethtool -L eth0 combined 8调整队列数量。注意这里的 8 只是队列数量不是速率“8 队列”意味着网卡的收包能力可以并行分摊到最多 8 个 CPU 上。那怎么知道系统当前实际在用几个队列最准确的办法还是看/proc/interrupts中该网卡拥有的中断号数量以及这些中断号的eth0-TxRx-*后缀编号。一版有几个队列就有几个独立的“处理通道”。网卡多队列数量必须结合你的 CPU 核数来匹配。队列数多于 CPU 核数没有意义因为最终处理软中断的核就那么多队列数太少又无法充分利用多核能力。一般推荐队列数等于物理 CPU 核数不包含超线程逻辑核或者等于 NUMA node 上的核数。3.2 结合 ethtool 调整队列数和流量分发固定好队列数量之后还要看网卡的 RSSReceive Side Scaling配置。RSS 是网卡硬件层面的哈希分发机制网卡根据数据包的五元组源 IP、目的 IP、源端口、目的端口、协议计算哈希值然后把包分配到对应的 RX 队列。这个哈希的好处是同一个 TCP 连接的数据包永远会进入同一个队列避免乱序又能让不同连接分到不同队列实现并行处理。RSS 的哈希配置在 ethtool 里可以查看和修改ethtool -x eth0 ethtool -X eth0 equal 4 ethtool -X eth0 weight 2 2 2 2ethtool -X eth0 equal 4表示让 4 个队列平均分担流量。weight模式则允许你用不同权重分配队列负载适用在队列对应的 CPU 能力不一致的场景比如某些 CPU 还在跑别的业务。哈希的依据可以通过ethtool -n eth0查看 flow-type如果确认需要 IPv6 或 UDP 的哈希需要在驱动或网卡配置中调整。在主流虚拟化环境里有一个常见坑虚机网卡virtio-net默认可能是单队列的。如果你在云服务器上做性能优化先确认ethtool -l eth0的 Combined 值。如果最大只有 1说明虚拟化层没开启多队列支持。AWS、阿里云这些平台的较新实例类型一般都支持 virtio 多队列但需要在镜像里配置 ethtool 设置。这个环节忘记做后续怎么调内核参数都是白费。RSS 队列哈希分布有一个很现实的陷阱当队列数不是 2 的幂时RSS 哈希的取模结果可能分布得“不均匀”。比如 3 个队列时哈希结果对 3 取模不同哈希段的大小不完全一致实际流量可能偏向某一队列。遇到这种情况优先用 4、8、16 这类 2 的幂次队列数。这是我实测中多次验证过的现象和 Toeplitz 哈希本身的分布特性有关。3.3 RPS/RFS 的适用场景RSS 是硬件层分流RPSReceive Packet Steering和 RFSReceive Flow Steering则是软件层的分流补充。RPS 没有硬件哈希它在内核收到数据包后通过计算哈希把数据包放到其他 CPU 的 backlog 队列中处理。RPS 适用于网卡本身不支持多队列的场合比如低端物理网卡或者虚拟网卡。启用 RPS 的方式是修改/sys/class/net/eth0/queues/rx-0/rps_cpus填写你想让该 RX 队列将数据包分发到的 CPU 掩码。例如要允许在 CPU0-3 之间分发echo f /sys/class/net/eth0/queues/rx-0/rps_cpusRPS 的代价是额外的 CPU 开销数据包从一个 CPU 的队列搬移到另一个 CPU会产生缓存失效和跨核调度。如果网卡本身已经有良好的 RSS 多队列再用 RPS 纯属画蛇添足。RFS 的数据包分发依据就不只是哈希了它是基于“哪个 CPU 正在运行接收该 socket 的应用程序”来动态选择队列。RFS 的意图是拉近数据包处理与用户态应用所在核心的距离。启用 RFS 需要两个参数配合一个是rps_flow_cnt另一个是flow_table_sizeecho 32768 /proc/sys/net/core/rps_flow_cnt启用 RFS 前先确认应用是否有明确的 CPU 亲和绑定。如果应用本身随机调度到不同核上RFS 反而会因为表项频繁变动而增加开销。RFS 是精细调优手段我一般只在数据库通信这类“应用固定绑核、网络流量大、包延迟敏感”的场景里开。4. 核心手段三内核收发路径与 sysctl 调优4.1 net.core.backlog 与网卡队列深度中断和队列分发解决了“哪个核处理”的问题但还没解决“能堆积多少包”的问题。网络栈的排队机制决定了在瞬时的流量洪峰下包是被处理、丢弃还是排队等待。这里面最常调的就是net.core.netdev_max_backlog它表示每个网络设备接收队列上最多可排队的包数量。默认值是 1000。为什么默认值不够用考虑一个场景某个瞬间网卡收到 3000 个包但 CPU 正忙于处理上一波数据这些包进入每个 CPU 的 backlog 队列。如果队列长度只有 1000那么后面核来的包只能直接丢进黑洞。相关日志会显示backlog exceeded。这时候合理的调整方式是把 backlog 加大到 3000 到 6000sysctl -w net.core.netdev_max_backlog4096但同样是那句老话不能无脑调大。netdev_max_backlog越大内存消耗越多且排队的包在队列里等待过久时TCP 层可能因为重传超时而误判网络异常反而触发拥塞控制收缩窗口。所以这个参数应该配合业务的实际突发程度来定——判断标准有一个关注网卡驱动丢包计数和 TCP 重传率若没有丢包就说明当前队列够用。另一个经常被忽视的参数是net.core.somaxconn。这个参数控制的是监听套接字listen socket的完成队列长度。它属于应用层接入调度而不是网卡调度的范畴但当高并发连接涌入时如果 somaxconn 太小内核会拒绝新的 TCP 连接请求表现为 connect 超时或 Connection reset。默认值 128 在高并发服务下明显不够。调整方法sysctl -w net.core.somaxconn4096这个参数需要应用配合因为应用层通常也用listen(fd, backlog)指定自己的队列深度内核实际生效的是两者中的较小值。Nginx 和 Redis 都有各自的 backlog 配置要一起改。4.2 网卡驱动参数与 Ring Buffer驱动层还有一个关键参数Ring Buffer 大小。Ring Buffer 是网卡驱动在内核内存中维护的环形缓冲区每个 TX/RX 队列都有一个。你可以在ethtool -g eth0中看到当前值和最大值$ ethtool -g eth0 Ring parameters for eth0: Pre-set maximums: RX: 4096 RX Mini: 0 RX Jumbo: 0 TX: 4096 Current hardware settings: RX: 2048 RX Mini: 0 RX Jumbo: 0 TX: 2048Ring Buffer 过小在瞬时大流量下网卡 DMA 写入包时没有空闲描述符就会丢包。调大它是最直接的办法ethtool -G eth0 rx 4096 tx 4096需要说明的是Ring Buffer 的调大不是线性的。RX Ring 太大的话中断合并coalesce可以积累更多包再上报延迟相对增大。现代网卡驱动中中断合并策略通常由ethtool -c eth0控制相关参数是rx-usecs和rx-frames。rx-usecs表示收到包后延迟多少微秒再产生中断rx-frames表示累计多少个包再中断。小包场景可以把 framing 调低来降低延迟大包流场景可以适当调高来减少中断次数降低 CPU 开销。这个位置最经典的选择题是“延迟优先还是吞吐优先”。比如游戏服务器场景要降低 rx-usecs而视频加速转码、文件传输这类大吞吐场景则更适合调高 rx-usecs换 CPU 使用率的下降。没有既降低延迟又提升吞吐的免费午餐网卡调优本质上是在中断频率和包处理延迟之间寻找平衡点。4.3 流量控制 qdisc 与 tc 命令“网卡调度”还有一个容易被忽略的方向数据包在发送队列里的排队规则qdisc。当应用往 socket 写入数据的速度超过网卡实际发送能力时数据包不是立即被网卡取走而是进入内核的发送队列。Linux 默认的 qdisc 叫pfifo_fast它基于数据包的 TOS 优先级字段做三次分类发送顺序是先按优先级再按 FIFO。对于纯网络转发场景基于多队列的mqprio可以替代单队列 qdisc让每个硬件队列对应一个独立调度规则。对于流量整形需求常见的 HTBHierarchical Token Bucket可以给不同业务分配不同带宽上限。用 tc 查看当前的 qdisctc qdisc show dev eth0一个常见的业务需求是“限速但允许突发”即保证某条业务流的带宽上限是 100Mbps但允许短时间内飙到 300Mbps。用 tbfToken Bucket Filter实现tc qdisc add dev eth0 root tbf rate 100mbit burst 384kb latency 50msHTB 的配置更复杂一些但思想是一致的通过令牌桶机制控制流量速率。这里我不推荐一上来就上复杂的 HTB因为 HTB 的层级和叶子类配置错误会导致整网卡 qdisc 异常连累所有流量。顺带提醒一个比较坑的问题高版本内核默认使用fq_codel作为部分环境下的默认 qdisc它对延迟友好但和某些网卡驱动的 TSO/GSO 组合起来会有奇怪的带宽下降。如果遇到“改完 qdisc 之后带宽反而下降”的问题先检查ethtool -k eth0里的 TSO 状态和 qdisc 的组合是否合理而不是马上质疑带宽配置本身。5. 实操案例配置前后对比一次真实调优过程5.1 测试环境与初始状态这里分享一个我调优过的实际环境一台 16 核的物理服务器配了两块万兆网卡主要业务是防火墙和数据转发。初始状态是系统默认配置网卡中断全部落在 CPU0 上RSS 有 16 个队列但没手工绑核irqbalance 开着Ring Buffer 默认 2048。测试方法是压测工具打满流量udp 包 100 万 pps、包长 64 字节。第一次跑结果是 CPU0 软中断占用直接飙到 80%而其余核心多数空闲丢包率为 2.3%平均延迟 180 微秒。这是典型的“单点瓶颈”表现——链路规格完全够就是 CPU 分布失衡导致处理不过来。看了一眼/proc/interruptseth0 的 16 个中断号全部显示在 CPU0 的统计列下快速增长。为什么会出现这个状态原因可能是启动时 BIOS 或 irqbalance 把中断默认分配到了 CPU0后来一直没有动过。另一个隐患是这台机器的网卡在 PCIe 拓扑上属于 Node 0CPU 0-7 对应 Node 0。后续绑定把它全部绑到 Node 0 的核上是最合理的。5.2 调整步骤与参数记录我在这个环境里做了下面这些操作顺序很重要先确认拓扑再改中断后调队列参数。第一步确认 NUMA 拓扑。lscpu | grep NUMA node输出显示 Node0 有 CPU0-7Node1 有 CPU8-15网卡对应的 PCIe 总线在 Node0。这意味着中断优先绑到 CPU0-7。第二步关闭 irqbalance 并保存中断亲和性设置。systemctl stop irqbalance systemctl disable irqbalance第三步把网卡 16 个中断号分别绑定到合适的 CPU。由于同一网卡多队列绑定在不同 CPU 上这里采用“按队列编号顺序绑 CPU”的策略队列 0 绑 CPU0队列 1 绑 CPU1……队列 7 绑 CPU7。接下来轮到队列 8依旧绑回 CPU0 或者其他核心这个回头再讨论。为了让命令简洁我写了一个循环for i in 78 79 80 81 82 83 84 85; do echo 1 /proc/irq/$i/smp_affinity; done数组中的值需要根据中断号对应 CPU0-7 的实际掩码来决定。如果队列数量多于核心数建议让多个队列均摊到同一组核心上不要把所有剩余队列都压到最后那个核。第四步调整 Ring Buffer 和中断合并ethtool -G eth0 rx 4096 tx 4096 ethtool -C eth0 rx-usecs 15 rx-frames 64选rx-usecs 15的原因是这个环境对延迟敏感既不想完全关闭合并策略那样 CPU 开销会很大也不能要太高的合并延迟。默认是 125 微秒15 微秒是“低延迟、适中 CPU 开销”的一个平衡点。第五步调整内核队列参数sysctl -w net.core.netdev_max_backlog4096 sysctl -w net.core.netdev_budget600 sysctl -w net.core.netdev_budget_usecs4000注意这里没有动somaxconn因为当前业务是转发型应用没有大量新增连接。第六步把这个配置写入/etc/sysctl.conf和 rc.local 持久化确保重启不丢。5.3 调整后的数据变化再次压测同样的流量结果如下指标调整前调整后变化CPU0 软中断占用80%15%明显下降整体 CPU 占用集中在 1 核8 核均衡负载分散丢包率2.3%0.02%大幅下降平均延迟180 微秒95 微秒降低 47%单核最大占用90%40%余量充足最关键的变化不是丢包率而是CPU 的富余度。调整之前这条链路已经“满载”只要流量再涨一点就会全面恶化调整之后单核最大占用降到 40%意味着同样硬件至少还能再扛一倍的流量这种余量对于突发流量至关重要。还有一个值得记录的细节在调整之前我曾尝试仅通过加大netdev_budget来改善丢包结果一度达到 0.00%但延迟飙升到 240 微秒。这说明单参数调优容易“按下葫芦浮起瓢”网卡调度必须多个维度配合。中断分散解决的是“谁在干”队列参数解决的是“能干多快”Ring Buffer 解决的是“能存多少”三者缺一不可。5.4 哪些配置不建议盲目去碰实践出真知网卡调度里有些参数是“危险的蜜糖”看起来很诱人实际容易出事。第一个是 TSO/GSO 的开关。TSOTCP Segmentation Offload能让网卡硬件从内核那里接管大包的 TCP 分段工作大幅降低 CPU 负载。默认开启没啥问题但如果你在抓包调试并且发现抓到的包比应用的 MTU 还大很多人会怀疑 TSO 有问题然后手动关闭。如果只是抓包分析正确做法是用ethtool -k eth0查看 offload 状态并临时关闭如果是生产环境不建议永久关闭 TSO除非你能明确证明是网卡驱动的 offload bug 导致的丢包或乱序。第二个是tcp_tw_reuse和tcp_tw_recycle。这俩和网卡调度没有直接关系但太多人把它们和“TCP 性能调优”捆在一起。尤其tcp_tw_recycle在 NAT 环境下会引发严重的连接问题导致对端校验时间戳失败而被丢弃数据。这个参数不要随便开启。tcp_tw_reuse相对温和但也只适合出站连接多的场景。第三个是彻底关闭中断合并。有的人为了极致延迟把rx-usecs设成 0就是“每个包都立刻中断”结果是 CPU 打满、吞吐暴跌。中断合并是网卡性能的重要权衡完全关闭等于自杀。第四个呢是误解 RSS 哈希的“均匀性”。RSS 是哈希分流而不是负载均衡器。它的目的是保持流的完整性不是保证包数均匀分布。如果某些大流量连接正好哈希到同一队列队列间负载不均是正常现象。RSS 不是万能的别指望它把每一条连接的负载都完美摊平。6. 常见问题与排查技巧实录6.1 软中断全堆在一个 CPU 上怎么处理这是最典型的网卡调度问题。处理时先别急着改配置建议按“看文件→分析→动手”的顺序来。第一步看/proc/interrupts确认是所有队列都堆在同一个核还是特殊的中断比如驱动管理的广播中断在某个核上。如果是全部堆在 CPU0多半是 smp_affinity 或者 irqbalance 没有生效。接着看是否真的绑核成功。如果修改了 smp_affinity 但中断依然增长在 CPU0可能是这个中断号被驱动主动忽略亲和性设置比如部分 MSI-X 中断在驱动初始化时就绑定了特定 CPU。这种情况下要么升级驱动要么用ethtool -L重建队列后重新绑定。如果是虚拟化环境还要检查宿主机的 vCPU 排队策略云厂商的多队列支持情况在此刻最容易露馅。如果确认亲和性没问题只是中断比较多可以尝试调整irqbalance的--hintpolicy参数从exact改为subset让 irqbalance 在“不完全破坏绑定”的前提下做平衡。当然如果已经决定手工管理还是关掉它最省心。6.2 网卡丢包但系统 CPU 空闲丢包不一定都是 CPU 瓶颈这是一件很容易被误解的事。如果 softirq 不高还有丢包优先检查这三个方向。方向一Ring Buffer 满。执行ethtool -S eth0 | grep -i drop或者ethtool -S eth0 | grep -i error看rx_missed、rx_no_buffer和rx_fifo_errors这类计数。如果这些计数在增长说明缓冲区太小加大 Ring Buffer。方向二backlog 队列溢出。查/proc/net/softnet_stat的第三列也就是 dropped 计数。这个计数增长时意味着netdev_max_backlog不够或者软中断处理速度跟不上了需要组合调整 backlog 和 budget。方向三应用层 socket 接收缓冲不足。net.core.rmem_max和net.ipv4.tcp_rmem控制内核为 socket 分配的接收缓冲区大小。如果 TCP 吞吐上不去但 CPU 和网卡层都“正常”问题很可能在这里。用ss -m查看当前 socket 的缓冲区占用确认是否已经到了上限。一个冷知识是有些虚拟化网卡的丢包计数不体现在 ethtool 里而是在宿主机的监控指标或者虚拟化层比如 XEN、KVM的统计里。云上碰到说不清的丢包先查实例规格的带宽上限很可能你跑满了承诺带宽被限流丢包是限流策略造成的。此时调内核参数毫无意义正确做法是升带宽规格。6.3 队列数翻倍之后性能反而下降另一种反直觉的情况是手动把队列数从 4 改成 8 或者 16性能不涨反降。这通常不是队列本身的错而是数据包哈希分布和队列之间的关联失效导致重排。当队列数变化时同一五元组的哈希会落到不同的队列。若应用层或者数据包捕获层依赖于“同一连接在同一队列”的稳定性切换队列会诱发乱序和重传。还有一个现实原因队列数增加导致每个队列都申请 Ring Buffer 内存内存占用翻倍不止。在 NUMA 环境下队列的内存必须和队列对应的 CPU 在同一节点。如果你只是把队列翻开没有同步绑定中断亲和性队列和 CPU 的 NUMA 错配会带来严重的内存跨节点访问CPU 开销大幅上升最终表现为性能下降。所以我一直强调一个原则队列数调整后必须立刻配套做亲和性绑定两者必须在同一次变更里完成。分开改出了问题就不知道是队列的问题还是绑定的问题。6.4 tcpdump 看不到包但业务却有流量这个问题经常和网卡调度搅在一起其实是 offload 特性在作怪因为驱动把大包拆分过程交给了网卡tcpdump 在软件层抓到的包可能是原始大包MTU 远大于接口的 1500导致抓包工具按默认 MTU 截断或过滤掉包。你抓不到不代表“没有包”只是包的位置和形态不在预想中。排查时先确认 offload 状态ethtool -k eth0 | grep tcp-segmentation-offload临时关闭后再抓包对比。我前面已说过生产环境不建议长期关闭 offload但在“既要抓包又要保证不干扰业务”的场合临时的关闭并重启抓包进程是常规操作。抓完之后记得恢复原状。另外一个容易被忽略的原因流量根本没进入内核协议栈而是在网卡层面被硬件过滤掉了。部分网卡支持 VLAN 过滤、ACL 过滤或者 flow-steering 规则。如果 tcpdump 看不到但业务确实在通十有八九是这些硬件的分流规则在起作用。检查ethtool -n eth0看是否有自定义规则。7. 最后再分享几个我在实际操作中的体会网卡调度这件事最忌讳的是从网上抄一堆 sysctl 参数就往生产环境里怼。每个参数改动都有代价必须先用压测工具建立基准确认瓶颈再逐个变量调整。我见过不少同事把文档里推荐的参数全开结果延迟不减反增、CPU 飙升最后只能一点点还原。正确思路是看/proc/interrupts和mpstat定位瓶颈是“中断不均”还是“队列溢出”然后只改对应的那一类参数。持久化配置千万别漏。手工改的 sysctl 和 ethtool 参数重启后会全部丢失。我的一般做法是用/etc/sysctl.d/99-network-tuning.conf保存 sysctl 配置用/etc/rc.local或者 systemd service 保存 ethtool 配置并且每次改动后跑一次“重启 验证”流程确保配置真的生效。压测工具的选择比你想的重要。我用iperf3测吞吐但它只能验证 TCP 大流性能对于“多连接并发、小包转发”这类网卡调度最敏感的场景性能差异要明显得多。建议同时使用pktgen或者dperf等支持多队列、多连接的工具观察不同队列均衡状况。没有这种压力测试你很难判断调整是否有效只能靠猜。最后不要迷信“最新内核一定最好”。网卡调度相关的行为在内核之间变化很大特别是网络子系统在引入lockless队列之后旧驱动和新内核的兼容性可能出问题。在升级内核之前先用相同的压测脚本在老内核上跑一遍作为基准。对比数据说话比任何发行版发布说明都靠谱。
