CPU 没跑满,机器却很卡:一个被忽略的指标
你有没有遇到过这种怪事:服务器 load average 飙得很高,网站响应变慢,可 top 里每个进程的 %CPU 加起来也就百分之三四十,机器明明「很闲」。这时候你盯着 CPU 使用率看半天也找不到凶手,因为凶手根本不在进程层面,而在软中断里。
网络包到达网卡之后,第一站是硬中断(IRQ)——网卡告诉内核「有数据了」。但硬中断的处理必须极短,它只做最紧急的事,然后把剩下的收包、协议栈处理工作「委托」给软中断(softirq)。当单位时间内到达的包太多,硬中断一个接一个,软中断就排起了长队,CPU 把大量时间花在 ksoftirqd 和 si(softirq)上。你在 top 里看到的 si 那一栏,就是这个时间占比。它高起来的时候,用户态进程确实拿不到 CPU,但你从「进程 %CPU」也是看不出来的。
先学会读 si 和 /proc/softirqs
最快的判断方式是 top(或 htop),看 CPU 行里的 si。经验阈值上,si 长时间超过 5% 就值得警惕,超过 20% 基本可以确定机器在软中断上烧 CPU,网站变慢就是它。要看得更细,直接读内核暴露的计数器:
# 观察一小段时间内的变化,而不是看累计值
watch -n 1 'cat /proc/softirqs'输出里有几个类别,重点关注这几行:
- NET_RX:网络收包软中断,最常成为瓶颈的那个。
- NET_TX:网络发包软中断。
- NET_RX 的分布形态:如果所有增长都压在
CPU0这一列,说明所有网卡的收包中断都打到了同一个核上,这就是典型的单核瓶颈——哪怕你有 8 核,网络处理也只有一个核在扛。
再看中断本身的分布:
# 看每个 CPU 收到的硬中断次数
grep -E 'CPU|eth0|ens|virtio' /proc/interrupts如果某个网卡的中断全部集中在 CPU0,那基本可以确诊了。这个现象的根源在于:很多虚拟化网卡、老网卡驱动默认只申请一个中断队列(queue),而操作系统默认把大部分中断路由到 CPU0。收包速率一高,CPU0 就在硬中断和软中断之间疲于奔命。
第一层解法:让硬件把中断分散到多核
如果你的网卡支持多队列(Multi-Queue),首选是让硬件自己分散。查看网卡支持的队列数:
ethtool -l eth0
# 或者更直观地看当前生效的组合数
ls /sys/class/net/eth0/queues/ | grep rxethtool -l 会输出 Pre-set maximums 和 Current hardware settings,一个是网卡能力上限,一个是当前生效值。如果 Current 的 RX 是 1 而 maximum 是 8,那就白瞎了硬件能力。把它调满,并分配对应的中断:
# 开 8 个收包队列
ethtool -L eth0 combined 8调完之后各队列会产生独立的 IRQ 号,可以手动把中断亲和性(SMP affinity)分配到不同 CPU:
# 查看队列对应的中断号
grep eth0 /proc/interrupts
# 把某个中断绑到 CPU2(bitmask 0x04 = 二进制 100)
echo 4 > /proc/irq/123/smp_affinity_list注意这里 smp_affinity 和 smp_affinity_list 是两种格式:前者是十六进制位掩码,后者是直接的 CPU 编号列表。用 smp_affinity_list 更不容易算错。手动绑定虽精确,但维护成本高,通常配合 irqbalance 更省事。
apt install -y irqbalance
systemctl enable --now irqbalanceirqbalance 会周期性地根据负载把中断迁移到空闲核上。它和手动绑亲和性二选一,不要同时用,否则两边打架。
第二层解法:RSS 不行就上 RPS
虚拟化环境里有个尴尬的现实:很多云主机的虚拟网卡(virtio-net)只有 1 个队列,ethtool -L 调不动,硬件层面根本没有多队列。这时候就要靠软件来分发,也就是 RPS(Receive Packet Steering)。
RPS 的原理是在软中断层面做「轮转分发」:单队列收上来的包,由内核按哈希分发到多个 CPU 的软中断队列上处理,从而把负载摊到多核。它不减少软中断总量,但能让多个核一起分担。配置方式很简单——往 /sys/class/net/eth0/queues/rx-0/rps_cpus 写一个 CPU 位掩码:
# 让 8 个 CPU(0-7)都参与 RPS,位掩码 0xff
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus位掩码怎么算?CPU0 是 1,CPU1 是 2,CPU2 是 4……第 n 个 CPU 对应 2^n,全都要就是把它们加起来。8 个核是 0xff,4 个核是 0xf,2 个核是 0x3。别扫错位置——是 queues/rx-0/ 下的文件,不是网卡根目录。
RPS 之外还有一个常被一起提到的 RFS(Receive Flow Steering):RPS 只按哈希分散到各个 CPU,RFS 进一步让同一个 TCP 连接的包尽量落到处理这个连接的那个 CPU 上,能提高 CPU 缓存命中率。RFS 的配置是调 /proc/sys/net/core/rps_sock_flow_entries(全局连接表大小)和每个 rx 队列的 rps_flow_cnt:
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt一个经验配比是:所有队列的 rps_flow_cnt 之和,不要超过全局 rps_sock_flow_entries。调太高只是浪费内存,调太低则连接表不够用,RFS 会退化成 RPS。
第三层:让配置活过重启
上面所有 sysfs 和 ethtool 的改动重启后就没了。生产环境必须做成持久化。systemd 系统有个现成的机制:/etc/systemd/network/ 或老的 /etc/rc.local,但更干净的做法是写一个 systemd oneshot 服务。
[Unit]
Description=Tune NIC multi-queue and RPS
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/nic-tune.sh
[Install]
WantedBy=multi-user.target脚本内容:
#!/bin/bash
set -euo pipefail
NIC=eth0
# 尝试开多队列,失败也不中断(单队列网卡会报错)
ethtool -L "$NIC" combined 8 || true
# 单队列时用 RPS 兜底
for q in /sys/class/net/$NIC/queues/rx-*/rps_cpus; do
[ -e "$q" ] && echo ff > "$q"
done
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for q in /sys/class/net/$NIC/queues/rx-*/rps_flow_cnt; do
[ -e "$q" ] && echo 4096 > "$q"
done注意脚本里 NIC 名字要按实际改(可能是 ens3、enp1s0、virtio0 等),用 ip link 确认。这类调整必须做成幂等的——重复执行不报错,因为服务重启时可能再跑一遍。
排障:别把 RPS 当银弹
调完 RPS 之后如果 si 还是高,先别急着加配置,按这个顺序排查:
- 确认瓶颈真在收包:
sar -n DEV 1看每秒包速率,对比网卡标称的 PPS(每秒包数)上限。小包同大包对 CPU 的压力天差地别——10Gbps 线速小包和最优化大包差了好几个数量级。如果 PPS 已经逼近线速,靠调 RPS 是治不好的,得考虑硬件分流或换网卡。 - 确认不是 conntrack 拖累:如果同时开了防火墙、NAT 或 Docker,每个包都要进
nf_conntrack查表,连接数一多,conntrack 表成为瓶颈,表现也是si高。看/proc/sys/net/netfilter/nf_conntrack_count对比nf_conntrack_max。 - 确认不是中断风暴:某些驱动在特定流量下会疯狂触发中断。看
vmstat 1的in(每秒中断次数)列,如果高得离谱(几十万),可能是中断合并(interrupt coalescing)没开。用ethtool -c eth0查看,ethtool -C eth0 rx-usecs 64之类适度合并。 - 确认 RPS 真的生效了:重新看
/proc/softirqs里 NET_RX 各 CPU 的增量。如果还是全压 CPU0,说明掩码写错位置了,或者被 irqbalance/其他脚本覆盖了。
CPU 亲和、NUMA 与网络栈的那些坑
调完多队列和 RPS,还有一个常被忽略的层面:多个 CPU 之间的数据流向。现代多核服务器普遍是 NUMA 架构——内存和 CPU 被划分成若干个「节点」,CPU 访问本地节点内存快,访问远端节点内存要跨 QPI/UPI 总线,慢一截。如果网卡接在 node 0,而收包的软中断却跑在 node 1 的核上,每个包的数据都要跨节点读一次,延迟和带宽都吃亏。
先看拓扑:
# 查看 NUMA 节点与各 CPU 归属
lscpu | grep -i numa
numactl --hardware
# 查看网卡挂在哪个 NUMA 节点
cat /sys/class/net/eth0/device/numa_node如果 numa_node 输出 -1,表示系统没开 NUMA(或是虚拟机),那就不用操心了。如果输出 0,就应尽量让这个网卡的中断和 RPS 掩码落在 node 0 的核上。用 numactl --hardware 能看到每个节点的 CPU 列表,把 RPS 掩码按这个列表来写,比一股脑写 0xff 更精准。虚拟机环境里多数没有真实 NUMA 拓扑,这一层可以跳过——但云厂商的裸金属实例上它非常重要。
另外别忘了 irqbalance 的一个常见副作用:它为了「均衡」,可能把网卡中断在节点之间来回迁移,反而打破 CPU 缓存热度。在追求极致网络性能的机器上,很多运维会选择关掉 irqbalance,改用静态绑核,把每个队列的中断固定到固定的核上。这两条路线各有利弊:irqbalance 省心但可能抖动,手绑核稳定但要跟 CPU 数量绑定、扩容时得重配。个人站长的单机场景,irqbalance 通常够用;只有在确定网络是瓶颈、且机器核数固定时,才值得上静态绑核。
用数据验证优化有没有生效
调参最忌讳改完不测。软中断这块有两个成本极低、收益极大的观测手段。
第一个是持续采样软中断增量:
# 每秒刷新,重点看 NET_RX 这一行各 CPU 的增量是否均衡
watch -n 1 'grep NET_RX /proc/softirqs'优化前,NET_RX 的增量几乎全在 CPU0 那一列;优化后,应该能看到增量分散到多列上。如果某一列的增量仍然占据绝对多数,说明要么掩码没写对,要么网卡队列数没真的调上去。这个对比是最直接的证据。
第二个是从系统层面看每秒中断与上下文切换:
vmstat 1
# in 列 = 每秒中断次数,cs 列 = 每秒上下文切换次数
sar -n DEV 1 # 每秒网络收发包速率(rxpck/s 是关键)rxpck/s(每秒收包数)这个数字决定了软中断的压力天花板。一个实用的判断是:单核处理小包的软中断能力大约在几十万 PPS 量级,超过这个数,单个核就一定扛不住,必须靠多队列/RPS 分担。把你的实际 PPS 和网卡能力、核数一对照,就能算出还有多少余量,也能提前判断扩容的临界点。这样下次访问量翻倍时,你是早就知道该加机器,而不是等它半夜打不开才发现。
小结
「卡但 CPU 不满」这个现象,本质是把网络处理的成本藏在软中断里了。标准的三层处理策略是:能开硬件多队列就开(ethtool -L),硬件不行就上 RPS 做软件分发,再进一步用 RFS 提升缓存命中;最后把配置用 systemd oneshot 固化,让它活过重启。诊断口诀就一句——先看 top 的 si,再看 /proc/softirqs 里 NET_RX 是不是全压在一个核上。看到这个分布,后面该动哪个开关就一目了然了。别迷信某个参数,先量出瓶颈到底在 PPS、conntrack 还是中断风暴,再对症下药。