别让一枚vCPU扛下所有软中断:VPS内核网络栈收包路径的极致调优
现象:CPU还闲着,softirq却爆了
过去一周我在一台轻云互联的KVM VPS上压测高并发网关,16 vCPU的配置,`top`里CPU整体才用了不到30%,但`si`(softirq)在CPU0上长期维持在80%以上。带宽每秒不过几百Mbps,可延迟曲线像心电图——偶发300ms以上的毛刺。用`mpstat -P ALL 1`一看,CPU0软中断占比85%,其余CPU都在看戏。问题不在带宽,而在VPS默认的收包路径:virtio网卡只开了1个队列,所有收包中断全砸在CPU0。
这不是云厂商的锅,而是很多VPS镜像里的内核参数和队列配置完全不适合网络密集场景。下面这套组合拳我实测把PPS提升了近4倍,且每个核的软中断分布均衡得让人舒适。
第一步:确认中断是不是“一边倒”
# 查看各vCPU的软中断占比
mpstat -P ALL 1 | awk 'NR>2 {print $3, $4, $5, $12}'
# 查看网卡中断分布
watch -n 1 'grep eth0 /proc/interrupts'
如果你看到所有中断都落在CPU0,同时`/proc/interrupts`里只有一个`virtio0`的条目,就说明只开了一个队列。VPS不像裸金属,不能动BIOS的MSI-X中断路由,但我们可以从内核层补。
第二步:解锁virtio多队列
# 查看当前队列数(比如显示1)
ethtool -l eth0
# 尝试扩展到等于vCPU数
ethtool -L eth0 combined $(nproc)
如果`ethtool -L`报错,说明虚拟化模板没开多队列,这只能联系云厂商。轻云互联的VPS模板默认就开启了virtio多队列,这步直接成功。开启后再看`/proc/interrupts`,每个队列都有独立的中断号了。
注意:开通多队列后,如果发现只有部分队列有流量,继续做第三步——它们只是“有资格”被调度,还需要RPS/XPS告诉内核如何路由。
第三步:RPS/XPS——让每个vCPU都接管自己的队列
RPS(Receive Packet Steering)用软件方式把收到的包均匀分发到指定vCPU的backlog,XPS则让发送队列绑定到对应vCPU,避免cache抖动。这套配置对VPS尤其关键,因为虚拟中断实际上还是靠eventfd通知的,真实触发核可能不均匀。
先算出CPU掩码(假设4核,即1111 = f):
# 如果8核,mask是ff;16核是ffff
# 先设置全局socket flow表
sysctl -w net.core.rps_sock_flow_entries=32768
# 为每个rx队列绑定所有vCPU,并启用4元组hash
for f in /sys/class/net/eth0/queues/rx-*/rps_cpus; do
echo f>"$f" # 4核用f,8核用ff,16核用ffff
done
for f in /sys/class/net/eth0/queues/rx-*/rps_flow_cnt; do
echo 4096>"$f"
done
# 为每个tx队列绑定对应编号的CPU(例如tx-0绑CPU0)
for i in /sys/class/net/eth0/queues/tx-*; do
idx=${i##*-}
echo $((1<<$idx)) >"$i/xps_cpus"
done
如果你的vCPU是超线程的兄弟核,最好把同一个物理核的兄弟核掩码填进去,避免跨物理核。但VPS里看不到拓扑时,直接用全部vCPU反而更安全。
第四步:中断合并和busy poll——延迟or吞吐,二选一
在多队列生效后,再调一下网卡的中断合并参数。VPS用virtio时,`ethtool -c`不一定全支持,但试试无妨:
# 追求吞吐:开启adaptive(动态合并)
ethtool -C eth0 adaptive-rx on adaptive-tx on
# 追求低延迟:彻底关合并,并为高吞吐场景开busy poll
ethtool -C eth0 rx-usecs 0 tx-usecs 0 adaptive-rx off adaptive-tx off
对于延迟敏感的网关类业务,我会再开内核busy poll。这把socket收包从“中断-唤醒”变成“主动轮询”,VPS上不用真正100%忙碌,只要调用`recvmsg`时顺手轮询一次:
sysctl -w net.core.busy_read=50 # 忙轮询时间上限,单位us
sysctl -w net.core.busy_poll=50
注意,不要无脑照抄。如果CPU本身已经吃紧,busy poll会使CPU占用率上升。我的建议:CPU有闲置余量时开busy poll;CPU已经满载时不如关掉它,省下CPU给应用层。
第五步:给内核软中断预算“加量”
这是大多数文章不会提的陷阱:默认`net.core.netdev_budget`只有300,`netdev_budget_usecs`是8000。在高PPS小包场景下,软中断处理包的数量很容易撞到预算上限,导致剩余包留到下个tick,延迟毛刺就是这么来的。
# 一次处理更多包,但不要太大(会拉长单核softirq时间)
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_budget_usecs=4000
# 如果你在低延迟环境,可以缩短usecs
sysctl -w net.core.netdev_budget_usecs=2000
调整完之后,用`mpstat -P ALL 1`观察,你会看到软中断像撒芝麻一样分散到所有vCPU。最后跑一遍你的压测或`iperf3`,延迟毛刺通常会明显改善。但要注意:如果业务是纯TCP长连接大文件传输,上面第3步的RPS收益有限,重点是第4步和第5步。
验证一次:从车间数据看效果
# 观察每个队列收到的包数是否均匀
ethtool -S eth0 | grep rx- | grep packets
在我那台轻云互联的VPS上,开启多队列+RPS/XPS后,单流iperf3从2.1Gbps升到3.4Gbps,而UDP小包PPS从18万跳到62万。最直观的是`top`里`si`从CPU0的80%变成了6个vCPU各占10%左右。
最后提醒一句:VPS上的内核优化离不开虚拟化层的配合,如果`/proc/interrupts`始终只显示一个中断号,那以上所有RPS/XPS都只是浅层缓解。真正的根治方式一定是让云主机支持virtio多队列——这也是我选择轻云互联来跑这类业务的原因:它的KVM模板没有阉割虚拟队列,内核调优才真正有落地空间。