
凌晨 2:40,香港葵涌机柜第 3 列第 18U 的那台主机又开始“闹脾气”:P99 延迟突然抬头、玩家掉线告警刷屏。我抱着工具包蹲在冷风口前,手摸着前置面板,看见那两块 NVMe 的指示灯闪得像迪厅,CPU0 的软中断竟然 90%+。那一刻我知道:该给中断分分家了。
下面这篇,是我那一夜到天亮的全部操作手记。它不是一篇“概念解释文”,而是我在 CentOS 7 上给电竞平台做 IRQ 亲和、NIC 与 NVMe 队列绑定、应用绑核 与 NUMA 优化的全流程。每个坑、每条命令、每一步可回滚方案,我都写上了。
硬件/软件/业务流量
| 项 | 参数 |
|---|---|
| 机型 | Dell R740xd(单机示例) |
| CPU | 2× Intel Xeon Silver 4210(10C20T/2.2GHz),共 20 物理核 / 40 逻辑核,NUMA=2 |
| 内存 | 128GB(2×64GB,均衡插槽) |
| 网卡 | Intel X710-DA2(10GbE ×2,驱动 i40e,多队列) |
| 存储 | 2× Samsung PM983 3.84TB NVMe(RAID-1 by mdadm,主要是热数据与临时盘;持久化在后端分布式存储) |
| 系统 | CentOS 7.9(3.10.0-1160 系列内核) |
| 关键服务 | 游戏网关(UDP/TCP 混合,短连接)、房间匹配服务、状态同步(WebSocket/UDP) |
| 流量特征 | 峰值 5~8 万 CPS,UDP 比例 70%,低延迟敏感 |
| 工具 | irqbalance、tuned、ethtool、numactl、fio、iperf3、sar、perf |
目标:把 NIC(收发包)、NVMe(日志/匹配临时数据)的中断与内核软中断从默认的“堆在 CPU0”改为跨核、按 NUMA 分区,尽量让中断与它们处理的数据、以及应用线程在同一 NUMA 节点内闭环,减少跨 NUMA 访问与抖动。
1. 基线:确认瓶颈点(别盲目“优化”)
先抓“病灶”:
# CPU/NUMA 拓扑
lscpu -e
numactl --hardware
# 中断分布(看 NIC/NVMe 的中断号和落在哪些 CPU 上)
grep -E "eth0|eth1|nvme" /proc/interrupts
# 网卡队列与驱动统计
ethtool -l eth0
ethtool -S eth0 | egrep "rx|tx|drop|busy|no_desc"
# 软中断热点
cat /proc/softirqs
top -H -p $(pidof ksoftirqd/0) # 看 ksoftirqd 开销(各 CPU)
# I/O 与网络延迟侧写
iostat -kx 1 10
nstat -a
我当时看到:
- eth0-TxRx-*、nvme0q* 大多落在 CPU0/CPU1;
- ksoftirqd/0 狂飙;
- ethtool -S 的 rx_no_desc 偶发(收包时队列忙);
- UDP P99 在 18~25ms(正常应 <10ms)。
2. 系统/BIOS 基础调优
2.1 BIOS(能进就进,效果立竿见影)
- 电源策略:Performance
- C-State:仅保留 C1/C1E,关闭深度 C-state(避免唤醒抖动)
- Turbo:开启(短期提升对突发流量友好)
- Memory Interleaving:Disabled(让 NUMA 拓扑更“真实”,配合线程绑核)
2.2 内核/系统
# 选一个低延迟基准
tuned-adm profile latency-performance
tuned-adm active
# 透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
如需更进一步的“极限低抖动”,可以在 GRUB2 增加:
isolcpus=managed,nohz_full=... rcu_nocbs=...
但这需要重启,本文主线不强依赖,先走“可在线生效”的 IRQ/队列亲和。
3. 管住 irqbalance(不然你刚绑好的就被“搬家”)
CentOS 7 缺省会跑 irqbalance。要么临时停掉,要么限制它能用的 CPU。
方案 A:停服务(简单粗暴)
systemctl stop irqbalance
systemctl disable irqbalance
方案 B:限制它不要碰我们规划的 CPU(推荐)
编辑 /etc/sysconfig/irqbalance:
# 把我们保留给"应用/关键中断"的 CPU 屏蔽掉(十六进制掩码)
# 例如屏蔽 CPU 0-1 与 20-23:掩码= (bits set for banned CPUs)
IRQBALANCE_BANNED_CPUS=0000000f,00000000
掩码很容易算错,更安全的做法是本文后面统一用 smp_affinity_list 的“CPU 列表”写法来设置中断亲和,不写十六进制。
改完后:
systemctl restart irqbalance
4. 网卡队列与 RSS/RPS/XPS:先把“水管”分宽
4.1 提升多队列数量(与 CPU 规划匹配)
# 查看支持
ethtool -l eth0
# 设为 8 RX / 8 TX(示例,根据 CPU 规划调整)
ethtool -L eth0 combined 8
4.2 关闭会增大延迟的特性(按业务定)
# LRO 会提高吞吐但恶化 UDP/短连接延迟
ethtool -K eth0 lro off gro on gso on tso on
4.3 RPS/XPS:让收发包在指定 CPU 上“就近”处理
# 假设我们准备把 eth0 的 rx0-7 绑到 CPU 2-9(示例)
for q in /sys/class/net/eth0/queues/rx-*; do
echo 2-9 > $q/rps_cpus
done
# 发包同理(XPS)
for q in /sys/class/net/eth0/queues/tx-*; do
echo 2-9 > $q/xps_cpus
done
rps_cpus/xps_cpus 支持 CPU 列表(如 2-9,12,14),避免十六进制出错。
5. NVMe 队列/调度:让磁盘中断别跑偏
# NVMe 使用多队列,调度器通常选 none
for d in /sys/block/nvme*n*/queue/scheduler; do
echo none > $d
done
# 把请求/完成回调尽量贴近 CPU(2 = 完全亲和)
for d in /sys/block/nvme*n*/queue/rq_affinity; do
echo 2 > $d
done
# 合理控制深度,防止排队抖动(按业务测试微调)
for d in /sys/block/nvme*n*/queue/nr_requests; do
echo 1024 > $d
done
6. IRQ CPU 亲和:核心操作(NIC + NVMe)
6.1 拿到各设备 IRQ 列表
# 网卡
ls -1 /sys/class/net/eth0/device/msi_irqs
# NVMe 控制器
ls -1 /sys/class/nvme/nvme0/device/msi_irqs
或:
grep -E "eth0|nvme0" /proc/interrupts
6.2 规划绑定策略(示例)
NUMA0(Socket0)负责 eth0 中断 + nvme0 中断 + 网关进程
NUMA1(Socket1)负责 业务计算线程(匹配/同步)
避免把同一对超线程兄弟核同时用于重载中断(例如 CPU2 与 CPU22 是 SMT 兄弟,就只选一个给硬中断,用另一个跑应用或软中断)
假设我们把中断主要绑到 CPU 2-11(NUMA0):
6.3 一键绑中断脚本(用 CPU 列表,避免十六进制掩码坑)
cat >/usr/local/sbin/irq_affinity_bind.sh <<'EOF'
#!/bin/bash
set -e
# 你可以按需微调
NIC=eth0
NVME_CTRL=nvme0
# 把 NIC 队列平均撒到 CPU 2-9
IRQS_NIC=$(ls -1 /sys/class/net/${NIC}/device/msi_irqs || true)
CPUS_NIC=(2 3 4 5 6 7 8 9)
i=0
for irq in $IRQS_NIC; do
cpu=${CPUS_NIC[$((i % ${#CPUS_NIC[@]}))]}
echo $cpu > /proc/irq/$irq/smp_affinity_list
echo "[NIC] IRQ $irq -> CPU $cpu"
i=$((i+1))
done
# 把 NVMe 中断绑到 CPU 10-11
IRQS_NVME=$(ls -1 /sys/class/nvme/${NVME_CTRL}/device/msi_irqs || true)
CPUS_NVME=(10 11)
i=0
for irq in $IRQS_NVME; do
cpu=${CPUS_NVME[$((i % ${#CPUS_NVME[@]}))]}
echo $cpu > /proc/irq/$irq/smp_affinity_list
echo "[NVME] IRQ $irq -> CPU $cpu"
i=$((i+1))
done
EOF
chmod +x /usr/local/sbin/irq_affinity_bind.sh
/usr/local/sbin/irq_affinity_bind.sh
校验:
grep -E "eth0|nvme0" /proc/interrupts
# 看列头 CPU2..CPU11 是否开始“吃”到中断计数
6.4 如果你必须写十六进制掩码(对老脚本兼容)
这里给一张易错就查表(CPU 0~15)的掩码对照:
| CPU | 掩码(hex) | CPU | 掩码(hex) |
|---|---|---|---|
| 0 | 00000001 | 8 | 00000100 |
| 1 | 00000002 | 9 | 00000200 |
| 2 | 00000004 | 10 | 00000400 |
| 3 | 00000008 | 11 | 00000800 |
| 4 | 00000010 | 12 | 00001000 |
| 5 | 00000020 | 13 | 00002000 |
| 6 | 00000040 | 14 | 00004000 |
| 7 | 00000080 | 15 | 00008000 |
写法:
# 例如把 IRQ 123 绑到 CPU 10
echo 00000400 > /proc/irq/123/smp_affinity
超过 32 核会用逗号分段的 32bit 块(低位在右),更容易写错,所以我还是强烈建议 smp_affinity_list。
7. 应用绑核与 NUMA 亲和:让“人”和“路”同城
电竞平台一般是网关最敏感。我的做法是:
- 网关进程(epoll/recv/send)跑在 NUMA0 的 12-17 核;
- 计算/匹配/状态同步跑在 NUMA1;
- 让 SO_REUSEPORT 的多进程/多线程对齐 XPS 队列,减少锁争用。
示例(systemd):
# /etc/systemd/system/gateway.service
[Unit]
Description=game-gateway
After=network.target
[Service]
User=game
ExecStart=/usr/bin/numactl --cpunodebind=0 --membind=0 \
/usr/local/bin/gateway --reuseport --workers=6
# 或者 taskset:
# ExecStart=/usr/bin/taskset -c 12-17 /usr/local/bin/gateway ...
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
8. 持久化与开机自愈(别靠记忆)
8.1 systemd 单元:在网卡就绪后再绑 IRQ
# /etc/systemd/system/irq-affinity-bind.service
[Unit]
Description=Bind IRQ affinity for NIC/NVMe
After=multi-user.target network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/irq_affinity_bind.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable irq-affinity-bind
8.2 rc.local 兜底(CentOS 7 可用)
echo "/usr/local/sbin/irq_affinity_bind.sh" >> /etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local
9. 验证:别用感觉,用数据说话
9.1 网络与中断
# 压测 10GbE,观察 drops/busy
iperf3 -s # 另一台做 client
iperf3 -c <server_ip> -u -b 6G -l 1200 -t 60
# 过程中看:
watch -n1 'grep -E "eth0|nvme0" /proc/interrupts | head -n 20'
watch -n1 'ethtool -S eth0 | egrep "rx_(no_desc|missed|errors)|tx_(busy|errors)"'
9.2 存储与调度
# 随机读写(按你的 NVMe 型号与盘符调整)
fio --name=randrw --filename=/data/testfile --size=20G \
--rw=randrw --rwmixread=70 --bs=4k --iodepth=64 --numjobs=4 \
--ioengine=libaio --direct=1 --runtime=60 --group_reporting
9.3 业务侧指标(真实玩家流量)
- P50/P95/P99 延迟
- 每核 ksoftirqd 占用
- UDP 丢包率
- 房间匹配耗时
我的一次前后对比(现场记要):
| 指标 | 调优前 | 调优后 |
|---|---|---|
| UDP P99(ms) | 18~25 | 7~10 |
| 网关进程 CPU 抖动(±%) | ±35% | ±10% |
rx_no_desc(/min) |
20~60 | 0~3 |
| ksoftirqd/0(%) | 90%+ | <15% |
| NVMe 平均完成时间(µs) | 280~320 | 180~220 |
10. 线上坑 & 解决过程(血泪合集)
irqbalance 抢回 IRQ
现象:你绑好了,半小时后又回 CPU0。
解法:确认 IRQBALANCE_BANNED_CPUS 生效;或干脆停服务;用 systemd 定时重申一次亲和(OnUnitActiveSec=5min)。
队列数 > 核心规划
现象:ethtool -L 设了 16 队列,但只给了 8 个 CPU,部分队列“共享”导致抖动。
解法:队列数 ≈ 亲和 CPU 数;宁少勿盲多。
SMT 兄弟核同绑硬中断
现象:一个物理核的两个超线程都跑硬中断,高峰期 cache 抖得厉害。
解法:给硬中断只选一个兄弟,另一个留给应用或软中断。
NUMA 远程内存访问
现象:IRQ 在 NUMA0,应用在 NUMA1,P99 偶发尖刺。
解法:numactl --cpunodebind/--membind 让数据路径同 NUMA。
十六进制掩码写错
现象:看起来绑定了,其实全落 CPU0。
解法:用 smp_affinity_list;多核机器的掩码分段顺序最易出错。
驱动差异(i40e/ixgbe)
现象:不同驱动的队列命名与统计字段不一致。
解法:脚本别写死,/sys/class/net/$IF/queues/* 走通配;统计指标用 ethtool -S 过滤关键字段。
GRO/LRO 开关拿不准
现象:关多了吞吐下滑,开多了延迟升高。
建议:UDP/实时赛况偏关(LRO off/GRO on),结合实际压测权衡。
11. 一页纸清单(复制即用)
# 0) 基线
lscpu -e; numactl --hardware
grep -E "eth0|nvme0" /proc/interrupts
ethtool -l eth0; ethtool -S eth0 | egrep "rx|tx|drop|busy"
# 1) 系统基调
tuned-adm profile latency-performance
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 2) 网卡
ethtool -L eth0 combined 8
ethtool -K eth0 lro off gro on
for q in /sys/class/net/eth0/queues/rx-*; do echo 2-9 > $q/rps_cpus; done
for q in /sys/class/net/eth0/queues/tx-*; do echo 2-9 > $q/xps_cpus; done
# 3) NVMe
for d in /sys/block/nvme*n*/queue/scheduler; do echo none > $d; done
for d in /sys/block/nvme*n*/queue/rq_affinity; do echo 2 > $d; done
for d in /sys/block/nvme*n*/queue/nr_requests; do echo 1024 > $d; done
# 4) IRQ 亲和(脚本)
/usr/local/sbin/irq_affinity_bind.sh
# 5) 应用绑核(示例)
numactl --cpunodebind=0 --membind=0 /usr/local/bin/gateway --reuseport --workers=6
12. 回滚策略(稳字当头)
systemctl stop irq-affinity-bind,重新启动 irqbalance:
systemctl enable irqbalance && systemctl start irqbalance
取消 RPS/XPS:
for q in /sys/class/net/eth0/queues/rx-*; do echo 0 > $q/rps_cpus; done
for q in /sys/class/net/eth0/queues/tx-*; do echo 0 > $q/xps_cpus; done
NVMe 调度还原(按默认需要):
for d in /sys/block/nvme*n*/queue/scheduler; do echo none > $d; done
凌晨 5:30,葵涌港口的灯像一条金线。监控面板上,P99 从 20ms 俯冲到 8ms,丢包线贴着 0 走。CPU0 不再孤军奋战,中断和线程各守各的地盘。
我把工具包合上,给那台主机拍了拍侧板。不是所有问题都需要换更强的硬件,有时候,只是你该告诉内核:“该谁干活,就让谁干。”
如果你也在为延迟、抖动、掉线抓狂,照着这篇从基线 → 队列 → IRQ → NUMA → 应用的顺序走一遍。遇到坑,别怕——风会一直吹,但你会越做越稳。