Linux 网卡多队列与中断绑定调优实战:RSS、RPS、RFS 到 XPS 完整指南

很多站长遇到过一个矛盾的现象:服务器 CPU 明明是多核的,用 top 看整体负载也不高,但网站的并发能力就是上不去,压测时 QPS 卡在一个数字死活突破不了。查来查去,最后发现瓶颈不在应用层,而在网卡——所有网络中断都挤在一个 CPU 核心上处理,那个核心 100% 满载,其余核心在打瞌睡。这篇文章专门讲这个问题:从硬件多队列(RSS)到内核软件分发(RPS/RFS),再到发送方向的 XPS,把网络中断负载均衡这条链路彻底打通。

一、问题是怎么产生的

先理解网卡收到数据包后的处理流程。一个数据包从网线进来,大致经过:

  1. 硬件中断(IRQ):网卡收到包,向 CPU 发中断,触发内核的网络中断处理程序。
  2. 软中断(softirq):为了不让中断处理占用太长时间影响其他任务,内核把耗时的部分放到软中断里执行,具体是 NET_RX_SOFTIRQ。执行软中断的上下文叫 ksoftirqd 内核线程。
  3. 协议栈处理:数据包经 IP、TCP/UDP 层处理,最终交给 socket。
  4. 应用读取:用户态进程通过 recv 把数据读走。

这套流程默认情况下有一个大问题:一个网卡(准确说是一个 RX 队列)只对应一个中断号,而这个中断号会被绑定到某一个 CPU 上处理。于是不管你有多少核心,单队列网卡的收包全都压在那一个核上。现代多核服务器上,单个核心处理网络中断的能力大概在几十万到一百万 pps 之间,一旦超过就出现丢包和延迟飙升。

解决办法分两层:硬件层用多队列把负载拆开(RSS),软件层用 RPS/RFS 让内核把处理分摊到多个核心。云服务器上你无法控制物理网卡,所以软件层的手段尤其重要。

二、先看清现状:中断分布与队列数量

排查第一步是确认当前的中断分布。查看网卡对应的中断号:

# 找到网卡的中断号
grep eth0 /proc/interrupts

# 输出示例
 24:   1842036   0   0   0   PCI-MSI 524288-edge  eth0-TxRx-0
 25:        12   902183   0   0   PCI-MSI 524289-edge  eth0-TxRx-1

这个输出的列对应各个 CPU 核心。上面这个例子很典型:队列 0 的中断几乎全在 CPU0 上(184万次),队列 1 全在 CPU1 上。这是正常的,因为每个队列天然绑定一个中断号,而中断号默认落在某个核上。

但如果看到这样的输出,就有问题了:

 24:   5812036   0   0   0   0   0   0   0   PCI-MSI 524288-edge  eth0-TxRx-0
 25:        12   0   0   0   0   0   0   0   PCI-MSI 524289-edge  eth0-TxRx-1

两个队列的中断全挤在 CPU0 上,其余 7 个核完全没参与收包。这就是典型的「单核瓶颈」。

再看网卡支持的队列数:

ls /sys/class/net/eth0/queues/
ethtool -l eth0

ethtool -l 会显示 Current 和 Pre-set maximums,告诉你当前用了几个 RX/TX 队列以及硬件上限。云服务器上常见的情况是:网卡宣称支持 8 个队列,但实际只启用了 1 到 2 个。

ethtool -S eth0 可以看每个队列的收包统计,确认流量是否真的分散:

ethtool -S eth0 | grep -E 'rx_queue_[0-9]+_packets'

三、查看软中断分布:mpstat 与 /proc/softirqs

中断号分布只是第一层。实际处理包的是软中断,而软中断可以在中断所在的核上执行,也可能被 ksoftirqd 线程接管。观察软中断分布:

cat /proc/softirqs

关注 NET_RX 那一行的各列数值,理想情况是各核比较均匀。如果某个核的 NET_RX 数量远超其他核,那就是收包瓶颈所在。

更直观的办法是用 mpstat%soft 列——这是软中断占用的 CPU 比例:

# 每 2 秒打印一次,共 5 次
mpstat -P ALL 2 5

输出中:

CPU    %usr   %sys   %soft  %idle
all    8.21   3.44    4.12   84.23
  0   12.34   6.21   22.45   58.99   <-- 软中断高
  1    2.11   0.88    0.42   96.59
  2    1.98   0.76    0.38   96.88
  3    2.05   0.81    0.40   96.74

CPU0 的 %soft 高达 22%,而其他核不到 0.5%。整机 idle 还有 84%,看起来「很闲」,但网络吞吐已经卡在 CPU0 上了。这个图表就是本文要解决的问题的最好证明。

如果没有 mpstat,装一下:

apt install sysstat          # Debian/Ubuntu
yum install sysstat          # CentOS/RHEL

四、第一层优化:手动绑定中断到不同核心

如果网卡有多个队列但中断都落在同一个核上,最直接的办法是手动改中断的 CPU 亲和性。每个中断号在 /proc/irq/<N>/smp_affinity 里有对应的掩码文件,值是十六进制位图。

先停掉自动均衡服务(它会覆盖你的设置):

systemctl stop irqbalance
systemctl disable irqbalance

然后写脚本把每个队列的中断分散到不同核:

#!/bin/bash
# /usr/local/bin/set_irq_affinity.sh
NIC="eth0"
CORES=(0 1 2 3)

i=0
for irq in $(grep "$NIC" /proc/interrupts | awk -F: '{print $1}'); do
    core=${CORES[$((i % ${#CORES[@]}))]}
    mask=$(printf "%x" $((1 << core)))
    echo "$mask" > /proc/irq/$irq/smp_affinity
    echo "IRQ $irq -> CPU $core (mask $mask)"
    i=$((i + 1))
done

注意 mask 是十六进制位图:1 表示 CPU0,2 表示 CPU1,4 表示 CPU2,8 表示 CPU3,f 表示 0-3 全部。脚本里用位移计算避免了手写错误。

如果要保留 irqbalance 的自动能力,可以用它的 --banirq 参数排除特定中断,或者配置 /etc/sysconfig/irqbalanceIRQBALANCE_BANNED_CPUS。但对小规模服务器来说,手动绑定更可控。

一个重要的细节:不要把网络中断绑到正在跑应用的核上。如果 PHP-FPM 和 Nginx 主要用 CPU0-1,那就把网卡中断分到 CPU2-3,减少争抢。当然对于核心数很少的机器(2 核 4 核),这做不到严格隔离,只能保证分布均匀。

五、第二层优化:RPS 让单队列网卡也能多核收包

云服务器的网卡往往是单队列或者队列数很少(虚拟化网卡的典型情况)。这时候中断绑定帮不上忙——只有一个中断,绑到一个核就完了。解决方案是 RPS(Receive Packet Steering),它让内核在软中断阶段把数据包「分发」到其他 CPU 的 backlog 队列里处理。

RPS 的配置在 /sys/class/net/eth0/queues/rx-0/rps_cpus,同样是十六进制掩码:

# 所有 CPU 都参与收包(4 核 = f)
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 8 核
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

写脚本通用化:

#!/bin/bash
# 自动计算所有 CPU 的掩码
NPROC=$(nproc)
# 位数向上取整到 4 的倍数,然后转十六进制
MASK=$(python3 -c "print(format((1 << $NPROC) - 1, 'x'))")
for q in /sys/class/net/eth0/queues/rx-*; do
    echo "$MASK" > "$q/rps_cpus"
done
echo "RPS 已设置为掩码 $MASK"

这里有个容易踩的坑:如果 nproc 返回 32 或更多,(1 << 32) - 1 的十六进制是 8 个 f,写法没问题;但掩码长度超过 CPU 数量时有些内核会截断或报错,所以稳妥的做法是取 $NPROC 与 32 的较小值。另外,对于已经支持多队列的网卡,不建议开 RPS,因为硬件 RSS 已经把包分发到各队列了,再叠加 RPS 只会增加一次跨 CPU 传输的开销。

RPS 还有一个配套参数 rps_flow_cnt,用来限制每个 CPU 上能跟踪的流数量:

echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

六、第三层优化:RFS 提升缓存局部性

RPS 解决了「分散处理」,但引入了一个新问题:数据包可能被分发到一个「没在运行对应应用线程」的 CPU 上,导致该 CPU 的 L1/L2 缓存里没有应用的数据结构,缓存命中率下降。RFS(Receive Flow Steering)解决的就是这个——它跟踪每个流(flow)是由哪个 CPU 上的应用读取的,然后把该流的包优先分发到那个 CPU。

启用 RFS 需要设置全局的流表大小 net.core.rps_sock_flow_entries

# 全局限定,建议 = 每个队列的 rps_flow_cnt × 队列数
sysctl -w net.core.rps_sock_flow_entries=32768

# 每个队列的流表大小
for q in /sys/class/net/eth0/queues/rx-*; do
    echo 4096 > "$q/rps_flow_cnt"
done

以 8 队列为例,8 × 4096 = 32768,正好对上全局值。如果队列数多,可以按比例调整,但注意流表是消耗内存的(每个表项约几十字节),32768 条占用不大,可以放心。

RFS 的收益在单连接高吞吐场景(比如大文件下载、数据库同步)比较明显,对于以短连接为主的小网站,收益相对有限。所以配置顺序上建议先做 RSS/中断绑定,再考虑 RPS,最后才是 RFS。

七、发送方向:XPS 与多队列 TX

前面讲的都是接收方向。发送方向的负载均衡叫 XPS(Transmit Packet Steering),配置在 /sys/class/net/eth0/queues/tx-*/xps_cpus

# 让每个 TX 队列对应不同的 CPU
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
echo 2 > /sys/class/net/eth0/queues/tx-1/xps_cpus
echo 4 > /sys/class/net/eth0/queues/tx-2/xps_cpus
echo 8 > /sys/class/net/eth0/queues/tx-3/xps_cpus

XPS 对反向代理、静态文件服务器这类「出流量远大于入流量」的场景很有用。默认情况下,应用的发送请求可能在任意 CPU 上执行,导致 TX 队列和 CPU 之间的映射混乱,增加锁竞争。

八、不能忽视的几个内核参数

中断处理能力还受几个内核参数制约,配合调整效果更好:

# 每个 CPU 的 backlog 队列长度,默认 1000,高并发下可提到 25000
net.core.netdev_max_backlog = 25000

# 网卡接收环形缓冲区大小(需要 ethtool 单独设)
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

# 中断合并:减少中断次数,提高吞吐但增加延迟
net.core.dev_weight = 64

环形缓冲区通过 ethtool 设置,这个值要看网卡支持范围:

ethtool -g eth0                     # 查看当前和最大值
ethtool -G eth0 rx 4096 tx 4096     # 设为最大值

缓冲区越大,突发流量下的抗丢包能力越强,代价是内存占用和延迟略有增加。对网站服务器来说,把 RX 缓冲区调大是性价比很高的操作。

另外,中断合并(interrupt coalescing)参数值得一调:

ethtool -c eth0
ethtool -C eth0 rx-usecs 16 tx-usecs 16

rx-usecs 表示收到包后延迟多少微秒再产生中断,把多个包合并处理。默认值通常偏高(如 50),对延迟敏感的网站可以降到 16 甚至更低;对吞吐优先的场景可以调高。

九、把配置固化下来

上面所有 /sys/proc 的写入都是临时生效,重启就没了。要持久化,有几种做法:

  • sysctl 参数:写进 /etc/sysctl.d/99-network-tuning.conf,然后 sysctl -p /etc/sysctl.d/99-network-tuning.conf
  • RPS/RFS/XPS 与中断绑定:写一个脚本,用 systemd service 在启动时执行。示例 unit:
    [Unit]
    Description=Network IRQ and RPS tuning
    After=network.target
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/bin/net_tuning.sh
    RemainAfterExit=yes
    
    [Install]
    WantedBy=multi-user.target
    放到 /etc/systemd/system/net-tuning.service,然后 systemctl enable --now net-tuning
  • ethtool 设置:非持久化,常见做法是在 systemd service 里一并执行,或者用 networkd-dispatcher / NetworkManager 的 dispatcher 脚本。

脚本内容整合前面所有操作:

#!/bin/bash
set -e
NIC="eth0"
NPROC=$(nproc)
MASK=$(python3 -c "print(format((1 << min($NPROC,32)) - 1, 'x'))")

# RPS
for q in /sys/class/net/$NIC/queues/rx-*; do
    [ -e "$q/rps_cpus" ] && echo "$MASK" > "$q/rps_cpus"
done

# RFS
sysctl -w net.core.rps_sock_flow_entries=32768 >/dev/null
for q in /sys/class/net/$NIC/queues/rx-*; do
    [ -e "$q/rps_flow_cnt" ] && echo 4096 > "$q/rps_flow_cnt"
done

# RX 缓冲
ethtool -G $NIC rx 4096 tx 4096 2>/dev/null || true

# 中断绑定(多队列网卡)
i=0
for irq in $(grep "$NIC" /proc/interrupts | awk -F: '{print $1}'); do
    core=$((i % NPROC))
    printf "%x" $((1 << core)) > /proc/irq/$irq/smp_affinity 2>/dev/null || true
    i=$((i + 1))
done
echo "network tuning applied"

十、验证优化效果

配置完成后要验证,否则等于没做。验证三件事:

  1. 中断分布是否均匀grep eth0 /proc/interrupts,看各列数值是否分散。
  2. 软中断是否均衡mpstat -P ALL 2 5,各核 %soft 应该接近,没有哪个核明显偏高。
  3. 吞吐是否提升:用 wrkab 压测,对比优化前后的 QPS 和错误率。

压测命令示例:

wrk -t4 -c200 -d30s --latency https://www.example.com/

如果优化前 QPS 卡在 3000、CPU0 软中断满载,优化后 QPS 提升到 8000+ 且各核软中断分布均匀,说明调整生效。如果 QPS 没变化,那瓶颈可能不在网卡,需要回头查应用层或数据库。

十一、小结

网络收包的负载均衡是一条从硬件到内核的链路:

  • 多队列网卡:用 numbers 或厂商工具启用多队列,再用中断绑定分散到不同核。
  • 单队列/虚拟网卡:用 RPS 把软中断处理分摊到多核。
  • 缓存局部性差:用 RFS 让包回到应用所在的 CPU。
  • 发送瓶颈:用 XPS 对齐 TX 队列与 CPU。
  • 基础参数:backlog、环形缓冲、中断合并一起调。

对个人站长来说,如果站点跑在 2 核或 4 核的低配 VPS 上,最值得做的一步其实是 RPS + netdev_max_backlog——两分钟就能配完,效果立竿见影。更高核心数的服务器再叠加中断绑定和 XPS。记住一个判断原则:如果 mpstat 显示只有个别核的 %soft 高而整机 idle 很高,那一定值得调网络中断;如果所有核都忙,那问题在别处,先解决应用层瓶颈。

Last modification:September 13th, 2026 at 07:56 am

Leave a Comment