刚接手一台新服务器时,我习惯先看一眼top和/proc/interrupts。很多人不明白,为什么要对一个“网卡调度”这么上心。我举个例子:同样的千兆带宽,默认配置下可能跑满 500Mbps 时 CPU 就飙到 80%,软中断(softirq)全压在一个核上,业务延迟忽高忽低;而调过中断亲和性和多队列参数之后,同样是这台机器,跑到 900Mbps 依然游刃有余。网卡调度不是玄学,它本质上解决的是“网络数据从网卡到应用程序之间,由谁、在哪个 CPU 上、以什么顺序处理”的问题。这篇文章我会从实际运维和排查经验出发,把这些关键点拆开来讲,适合刚接触 Linux 网络性能调优的工程师,也适合已经踩过坑、想系统地理解链路原理的人。
Linux 网卡调度
1. 先搞清楚:网卡调度到底在管什么
1.1 从收包第一步说起
如果你在裸机上抓过包,或者用tcpdump观察过高速流量,很快就会遇到一个问题:数据包到了网卡,网卡有没有直接把它“喂”给应用程序?答案是没有。数据包从物理线缆进入网卡,在网卡内存中先被暂存,然后通过 DMA(Direct Memory Access)写入内核分配好的内存缓冲区,这个过程不占用 CPU。之后网卡会发出一个中断信号,通知 CPU“有数据来了,请处理”。
中断触发之后,才是“调度”真正的开始。内核收到中断信号后,要决定由哪一个 CPU 核心来处理这个中断。如果所有中断都落到 Core 0,那么所有网络栈处理都堆在这个核上。你可以在top里按1看每个 CPU 的负载,就会看到那一个核的si(软中断)占用特别高,其余核却很闲。这就是经典的“中断不均”问题。
这里的核心概念值得展开说一下:硬中断(hardirq)是网卡硬件发出来的,CPU 必须立刻响应;软中断(softirq)是内核在硬中断之后安排的延后处理流程,真正的协议栈解析、数据分发就是在软中断里做的。Linux 内核通过net_rx_action这个函数把驱动收到的数据包从队列里取出来,一层层送进 IP 层、TCP/UDP 层。网卡调度,核心就是管理这些硬中断和软中断在不同 CPU 上的分配,以及驱动程序向内核提交数据包的节奏。
还有一个容易误会的点:很多人以为数据包被中断之后就“直达”应用进程了,中途没有别的调度。实际上,内核还涉及“接收队列”和“处理顺序”的调度。不同进程绑定在哪个 CPU,以及网络数据最终送到哪个 socket 队列,会受到 RPS(Receive Packet Steering)机制的影响。简单说,从包进网卡,到 enter 到用户态 socket,整个链路里有两到三处可以“分流”的地方,每一处都对应不同的调度手段。
用生活中的类比来理解:网卡好比快递总站,数据包是快递。中断就是快递到了之后总站前台喊了一嗓子“来活了”。如果只有一个前台(一个 CPU 核心)接单,所有快递搬运、分拣、派发都得走这一个窗口,其他窗口闲得无聊,那吞吐自然受限。多队列、多核调度,本质就是多开几个前台,同时按规则分快递,让每个窗口干自己该干的活。
1.2 中断 vs 轮询:NAPI 的历史意义
早期网卡驱动处理数据包的方式是“一个包一次中断”。当网络速率较低时,这种方式没有太大问题;但当网卡达到千兆甚至更高速率时,这个模式就变成了灾难——每来一个包就打断 CPU 一次,高频中断让 CPU 忙于上下文切换,几乎没时间干正事。Linux 内核后来引入了 NAPI(New API),核心思路是:高负载下从“中断驱动”切换到“轮询驱动”。
NAPI 的工作方式可以用一句话概括:网卡收到第一个包时依然会发中断,但 CPU 进入中断处理流程后,会立刻关闭该网卡后续的 RX 中断,然后以轮询方式从网卡队列里批量取包处理;直到队列清空或者处理配额用完,才重新开启中断。这个机制背后是对硬中断数量的大幅压缩——把“来一个包处理一次”变成“来一批包集中处理一次”。
这个设计对“网卡调度”的意义太大了。没有 NAPI,谈多队列、谈软中断均衡都是空谈,因为 CPU 光服务硬中断就忙不过来了。有了 NAPI,CPU 才有余量去精细地做负载分配。所以当你调优网卡性能时,首先得确认自己的驱动是不是基于 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/RFS
3.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 调整队列数和流量分发
固定好队列数量之后,还要看网卡的 RSS(Receive 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 是硬件层分流,RPS(Receive Packet Steering)和 RFS(Receive 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_size:
echo 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 到 6000:
sysctl -w net.core.netdev_max_backlog=4096但同样是那句老话,不能无脑调大。netdev_max_backlog越大,内存消耗越多,且排队的包在队列里等待过久时,TCP 层可能因为重传超时而误判网络异常,反而触发拥塞控制收缩窗口。所以这个参数应该配合业务的实际突发程度来定——判断标准有一个:关注网卡驱动丢包计数和 TCP 重传率,若没有丢包就说明当前队列够用。
另一个经常被忽视的参数是net.core.somaxconn。这个参数控制的是监听套接字(listen socket)的完成队列长度。它属于应用层接入调度而不是网卡调度的范畴,但当高并发连接涌入时,如果 somaxconn 太小,内核会拒绝新的 TCP 连接请求,表现为 connect 超时或 Connection reset。默认值 128 在高并发服务下明显不够。调整方法:
sysctl -w net.core.somaxconn=4096这个参数需要应用配合,因为应用层通常也用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,让每个硬件队列对应一个独立调度规则。对于流量整形需求,常见的 HTB(Hierarchical Token Bucket)可以给不同业务分配不同带宽上限。用 tc 查看当前的 qdisc:
tc qdisc show dev eth0一个常见的业务需求是“限速但允许突发”,即保证某条业务流的带宽上限是 100Mbps,但允许短时间内飙到 300Mbps。用 tbf(Token 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/interrupts,eth0 的 16 个中断号全部显示在 CPU0 的统计列下快速增长。为什么会出现这个状态?原因可能是启动时 BIOS 或 irqbalance 把中断默认分配到了 CPU0,后来一直没有动过。另一个隐患是,这台机器的网卡在 PCIe 拓扑上属于 Node 0,CPU 0-7 对应 Node 0。后续绑定把它全部绑到 Node 0 的核上是最合理的。
5.2 调整步骤与参数记录
我在这个环境里做了下面这些操作,顺序很重要,先确认拓扑,再改中断,后调队列参数。
第一步,确认 NUMA 拓扑。
lscpu | grep "NUMA node"输出显示 Node0 有 CPU0-7,Node1 有 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_backlog=4096 sysctl -w net.core.netdev_budget=600 sysctl -w net.core.netdev_budget_usecs=4000注意,这里没有动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 的开关。TSO(TCP 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队列之后,旧驱动和新内核的兼容性可能出问题。在升级内核之前,先用相同的压测脚本在老内核上跑一遍作为基准。对比数据说话,比任何发行版发布说明都靠谱。