部署在香港服务器 CentOS 上的电竞平台,我是如何用 IRQ CPU 亲和把 I/O 拉满的(完整实操 + 优化心得)
技术教程 2025-09-20 10:20 215


凌晨 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%,低延迟敏感
工具 irqbalancetunedethtoolnumactlfioiperf3sarperf

目标:把 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 → 应用的顺序走一遍。遇到坑,别怕——风会一直吹,但你会越做越稳。