香港CN2服务器CentOS 7 实战:用 IRQ CPU 亲和与 RPS/RSS 均衡,压缩网游服务器软中断尾延迟

夜里 1:30,机房的冷气像一面墙。玩家工单刷屏:跨服战团战抖动、瞬移、掉线。我站在湾仔的机柜前,盯着那台跑在 CN2 线路上的物理机,PING 抖成一根锯齿。我知道这不是“网络不好”四个字能打发的事——更像是我们自己内核里的软中断和 IRQ 在内部打架。那一夜我从“怀疑网卡队列”到“手动配 IRQ CPU 亲和”的全过程,后来成了我们团队在香港节点的标准操作。下面就是我完整的实操教程与血泪踩坑史。
硬件与环境(真实现场参数)
- 机房/线路:香港机房,CN2 GIA 出口。
- 系统:CentOS 7.9 (3.10.0-1160.el7.x86_64);irqbalance 默认安装。
- CPU:Intel Xeon E-2288G(8C16T,单路,NUMA 1 节点)。
- 内存/磁盘:64GB DDR4;系统盘 SATA,业务盘 2× NVMe(RAID1)。
- 网卡:Intel X520-DA2 10GbE(ixgbe 驱动),上行接运营商接入交换机(万兆)。
- 业务:中大型网游逻辑服 + 网关服(UDP 为主,少量 TCP)。
- 进程模型:多进程网关,每进程绑核,跨服逻辑服独立。
诊断:为什么是 IRQ & 软中断?
我先看了 CPU 和软中断热点:
mpstat -P ALL 1
pidstat -w -p ALL 1 | egrep 'ksoftirqd|softirq'
ksoftirqd/0 和 ksoftirqd/1 飙高,而业务进程 CPU 却不满。/proc/interrupts 里 eth0 的 TxRx-0/TxRx-1 中断扎堆在 CPU0/1,明显失衡。再看网卡统计:
ethtool -S eth0 | egrep 'rx_missed|rx_no|rx_queue'
能看到个别队列有 rx_no_buffer_count 累积。症状:
- 延迟 p99 抖动;
- 收包丢在软中断队列;
- 单核扛过多 RX 队列导致爆点。
- 问题落在中断分配与 CPU 亲和,以及收发队列(RSS/RPS)配置上。
总体方案
- 扩展并均衡网卡队列(硬件 RSS 能开的全开)。
- 关闭 irqbalance 或限制它,改为手工绑定每个 IRQ 到合适的 CPU。
- 让软中断与业务进程尽量分隔核位,降低抖动(NUMA 同节点)。
- 校准内核网络参数(netdev_budget、RPS/RFS、backlog、UDP 缓冲)。
- 谨慎调整网卡 offload(只对延迟敏感路径关必要项)。
- 落地成脚本 + systemd,重启也不怕丢配置。
观测与回滚准备充分。
Step 1:确认 NUMA 与队列能力
lscpu -e=CPU,CORE,SOCKET,NODE,ONLINE
numactl -H
cat /sys/class/net/eth0/device/numa_node
ethtool -l eth0 # 当前 channels
ethtool -i eth0 # 驱动与固件
我的机器单 NUMA(node0),ixgbe 支持较多收发队列。为了后续均衡,我把队列设为与物理核数一致(8):
ethtool -L eth0 combined 8
注:combined 表示把 RX/TX 配成对,硬件 RSS 能力足的情况下,这样更容易对齐中断与核。
Step 2:限制 irqbalance,避免“抢活儿”
CentOS 7 默认有 irqbalance,它会根据 affinity_hint 和负载迁移中断,但对低延迟场景常常“瞎好心”。两种做法:
做法 A(推荐):保留 irqbalance,但屏蔽我们不想让它碰的核。
编辑 /etc/sysconfig/irqbalance:
# 把 0 和 1 两个逻辑核留给系统与守护进程,不让 irqbalance 使用
IRQBALANCE_BANNED_CPUS=00000003
00000003 的二进制低两位为 1 → ban CPU0、CPU1。(根据你实际想 ban 的核改掩码)
做法 B(更极端):直接停用 irqbalance
systemctl stop irqbalance
systemctl disable irqbalance
我现场选择 B,因为要精确手配每条队列的 IRQ 亲和。
Step 3:关闭影响延迟的 offload(只关必要的)
对 UDP/小包低延迟业务,我只关 GRO,保留 TSO/GSO 以免 CPU 飙升:
ethtool -k eth0 | egrep 'gro|gso|tso|lro'
ethtool -K eth0 gro off lro off
# 如 TCP 传大文件性能很关键,再评估是否保留 tso/gso;我一般保留。
Step 4:设置 RPS/RFS(软件侧流量均衡)
硬件 RSS 有时对 UDP 哈希不理想,RPS/RFS 作为“二次分流”。我给每个 RX 队列的 rps_cpus 绑定到一组 CPU(与 IRQ 分配一致):
# 全局 flow entries
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
# 为每个 RX 队列分配 4096 条
for q in /sys/class/net/eth0/queues/rx-*; do
echo 4096 > $q/rps_flow_cnt
done
rps_cpus 的 CPU mask 我在 Step 6 里统一生成。
Step 5:优化网络内核参数(CentOS 7 / 3.10 内核)
创建 /etc/sysctl.d/90-game-net.conf:
# 提高软中断每轮处理预算
net.core.netdev_budget=600
net.core.netdev_budget_usecs=8000
# 提升输入队列消化能力
net.core.netdev_max_backlog=50000
# UDP 缓冲
net.core.rmem_default=262144
net.core.rmem_max=268435456
net.core.wmem_default=262144
net.core.wmem_max=268435456
net.ipv4.udp_mem= 262144 4194304 8388608
net.ipv4.udp_rmem_min=16384
net.ipv4.udp_wmem_min=16384
# 端口范围(视业务需要)
net.ipv4.ip_local_port_range=10000 65000
# 拒绝源路由、重定向(安全硬化)
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
生效:
sysctl --system
Step 6:核心——手工绑定 IRQ → CPU(并配 RPS 掩码)
6.1 拿到网卡队列对应的 IRQ 号
grep -E "eth0|ixgbe" /proc/interrupts
# 或更精确:
ls -1 /sys/class/net/eth0/device/msi_irqs/
# 输出如: 123 124 125 ... (每个是一个 MSI-X IRQ)
我习惯用 IRQ 名称中的 TxRx-N 做队列序号(ixgbe/多驱动通用)。目标:
- 把 RX 中断分散到 CPU2-CPU9(举例);
- 保留 CPU0/1 给系统与日志;
- 把 业务进程固定在 CPU10-15(示例)避免和软中断抢核。
- 若你的机器只有 8 逻辑核,就相应缩小区间,但仍要“分离业务核与 IRQ 核”。
6.2 掩码计算规则(十六进制)
- CPU0 对应 bit0 → 掩码 0x1
- CPU1 → 0x2
- CPU2 → 0x4
- … 以此类推
- 多核组合 → 各 bit 位 OR。
- 64+ 逻辑核需要写 smp_affinity_list 更直观(如 2-7,12-15)。
6.3 一把梭脚本(适配 ixgbe / 通用 MSI-X 命名)
保存为 /usr/local/sbin/set-irq-affinity.sh:
#!/usr/bin/env bash
# 绑定 eth0 的每个 queue IRQ 到指定 CPU,顺带设置 RPS
# 使用方式:set-irq-affinity.sh eth0 2-9
set -euo pipefail
IFACE=${1:-eth0}
CPULIST=${2:-2-9} # 可写成 "2,3,4,5,6,7,8,9" 或 "2-9"
# 展开 CPU 列表成数组
expand_cpulist() {
local list=$1 out=()
IFS=',' read -ra parts <<< "$list"
for p in "${parts[@]}"; do
if [[ "$p" =~ ^([0-9]+)-([0-9]+)$ ]]; then
for ((i=${BASH_REMATCH[1]}; i<=${BASH_REMATCH[2]}; i++)); do out+=($i); done
else
out+=($p)
fi
done
echo "${out[@]}"
}
cpus=( $(expand_cpulist "$CPULIST") )
ncpu=${#cpus[@]}
if [[ $ncpu -eq 0 ]]; then
echo "No CPUs parsed from $CPULIST"; exit 1
fi
# 找到该网卡的全部 MSI-X IRQ;按队列序排列
irqs=($(ls -1 /sys/class/net/${IFACE}/device/msi_irqs/ | sort -n))
if [[ ${#irqs[@]} -eq 0 ]]; then
echo "No IRQs found for ${IFACE}. Are you using MSI-X?"; exit 1
fi
# 逐个队列绑定到一个 CPU(轮询分配)
for i in "${!irqs[@]}"; do
irq=${irqs[$i]}
cpu=${cpus[$(( i % ncpu ))]}
# 写 smp_affinity 或 smp_affinity_list(优先用 list)
if [[ -f /proc/irq/${irq}/smp_affinity_list ]]; then
echo ${cpu} > /proc/irq/${irq}/smp_affinity_list
else
# 把 cpu 转成 mask
mask=$((1<<cpu))
printf "%x" ${mask} > /proc/irq/${irq}/smp_affinity
fi
echo "IRQ ${irq} -> CPU ${cpu}"
done
# RPS:每个 RX 队列设置 rps_cpus,选与其绑定 CPU 相同的掩码
# 若队列名不是 rx-*, 可按需调整
rxqs=(/sys/class/net/${IFACE}/queues/rx-*)
for i in "${!rxqs[@]}"; do
cpu=${cpus[$(( i % ncpu ))]}
# 生成 rps_cpus 掩码(最多支持 64 核;更大场景建议写 rps_cpus_list)
mask=$((1<<cpu))
printf "%x" ${mask} > ${rxqs[$i]}/rps_cpus
echo 4096 > ${rxqs[$i]}/rps_flow_cnt
echo "RPS ${rxqs[$i]} -> CPU ${cpu}"
done
赋权并执行:
chmod +x /usr/local/sbin/set-irq-affinity.sh
/usr/local/sbin/set-irq-affinity.sh eth0 2-9
小技巧:超线程同核(如 CPU2 和 CPU10 是 HT 兄弟)不要同时放中断与业务,避免资源争用。用 lscpu -e 看 CORE 列来配对。
6.4 开机自启动(systemd)
/etc/systemd/system/irq-affinity.service:
[Unit]
Description=Set IRQ CPU Affinity for eth0
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/set-irq-affinity.sh eth0 2-9
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now irq-affinity.service
Step 7:业务进程绑核与内存就近(NUMA 亲和)
我将网关/逻辑服避开 2-9(让给 IRQ/软中断),比如绑到 10-15:
# 单进程示例
taskset -c 10-15 numactl --cpunodebind=0 --membind=0 ./game_gateway --config gateway.yaml
多进程就切片分配,保证进程核与IRQ 核****不重叠,并且都在同一个 NUMA 节点(本机单节点更简单)。
Step 8:可选的更进一步(重启级别)
tuned:tuned-adm profile latency-performance(降低 C-State)
BIOS:关 C-States、开 Performance governor。
GRUB 内核参数(需评估):
- isolcpus= 把业务核从调度器里隔离出来;
- nohz_full=、rcu_nocbs= 进一步降抖(3.10 上可用但不如新内核顺手)。
- 这些改动回报高,但涉及重启与回退预案,我在香港节点是分批灰度做的。
观测与对比:前后效果(真实口径)
采样 15 分钟,高峰期(战团战):
| 指标 | 调优前 | 调优后 |
|---|---|---|
| Ping p99(内地电信→HK,ms) | 92.4 | 78.1 |
| 网关 UDP p99 处理时延(μs) | 1430 | 710 |
/proc/net/softnet_stat 丢包(条/分钟) |
800~1200 | <50 |
ethtool -S 单队列 rx_no_buffer_count(累计/15min) |
高于 2k 的队列有 3 个 | 全部 <200 |
CPU0 ksoftirqd/0 平均占用 |
35% | 8% |
| 玩家断线率(千分比) | 2.6‰ | 0.9‰ |
注:Ping 改善不是“改 CN2 线路”,而是本机丢软中断/排队少了,端到端尾延迟自然回落。
典型坑与现场解法
irqbalance 悄悄改回来了
某次补丁升级后它被重新启用,队列回到 CPU0/1。**解法:**完全 disable 并加 systemd 任务,巡检脚本校验 /proc/irq/*/smp_affinity_list。
队列名不一致(有的驱动不是 TxRx-N)
解法:脚本改成扫描 /sys/class/net/IFACE/device/msi_irqs,不依赖命名;必要时结合 grep IFACE /proc/interrupts 做映射。
超线程绑错(IRQ 与业务落到同一个物理核的两个线程)
现象:p99 一直抖。解法:用 lscpu -e 看同 CORE 的兄弟,分开放。
GRO 全关导致 TCP 大流量时 CPU 飙高
解法:只关 gro,保留 tso/gso;或在非高峰开启 gro 做大包合并,峰值再关。
RPS 太激进
rps_cpus 配太广,反而增加跨核 cache 抖动。**解法:**与 IRQ 同核或同 NUMA 小范围配置即可。
ethtool -L 失败(设备忙或驱动不支持)
解法:低峰短暂 down/up 网卡:ifdown eth0 && ifup eth0;或升级固件/驱动(在备机先验)。
sysctl 被其他配置覆盖
某些镜像里有 99-sysctl.conf。**解法:**统一放在 90-game-net.conf 并纳入 CM 管理,发布后 sysctl --system 校验。
我落地用的“巡检清单”(截取核心项)
# 1) 队列数与亲和
ethtool -l eth0
grep eth0 /proc/interrupts
for i in /sys/class/net/eth0/device/msi_irqs/*; do
n=$(basename $i)
echo -n "IRQ $n -> "
cat /proc/irq/$n/smp_affinity_list
done
# 2) 软中断丢包/压力
watch -n 1 cat /proc/net/softnet_stat # 观察第2列(丢弃)与第10列(时间上限)
pidstat -w -p ALL 1 | egrep 'ksoftirqd|softirq'
# 3) 网卡统计
ethtool -S eth0 | egrep 'rx_no|rx_missed|queue_[0-9]+_rx_packets'
# 4) 业务进程绑核
ps -eo pid,cmd,psr | egrep 'game_gateway|game_logic'
# 5) NUMA
numactl -H
cat /sys/class/net/eth0/device/numa_node
完整一次性部署脚本(把本文关键操作串起来)
先在备机/低峰验证!
#!/usr/bin/env bash
set -euo pipefail
IFACE=${1:-eth0}
QUEUE=${2:-8} # 目标 combined 队列
IRQ_CPUS=${3:-2-9} # 用于 IRQ/RPS 的 CPU 列表
# 1) 调整队列
ethtool -L ${IFACE} combined ${QUEUE} || echo "[WARN] ethtool -L failed, check driver/firmware"
# 2) 只关必要 offload
ethtool -K ${IFACE} gro off lro off || true
# 3) sysctl
cat >/etc/sysctl.d/90-game-net.conf <<'EOF'
net.core.netdev_budget=600
net.core.netdev_budget_usecs=8000
net.core.netdev_max_backlog=50000
net.core.rmem_default=262144
net.core.rmem_max=268435456
net.core.wmem_default=262144
net.core.wmem_max=268435456
net.ipv4.udp_mem=262144 4194304 8388608
net.ipv4.udp_rmem_min=16384
net.ipv4.udp_wmem_min=16384
net.ipv4.ip_local_port_range=10000 65000
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
EOF
sysctl --system
# 4) RPS/RFS 全局
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for q in /sys/class/net/${IFACE}/queues/rx-*; do
echo 4096 > $q/rps_flow_cnt
done
# 5) 绑定 IRQ/RPS
install -m 0755 /dev/stdin /usr/local/sbin/set-irq-affinity.sh <<'EOS'
<把前文 set-irq-affinity.sh 粘进来>
EOS
/usr/local/sbin/set-irq-affinity.sh ${IFACE} ${IRQ_CPUS}
# 6) systemd 挂载
cat >/etc/systemd/system/irq-affinity.service <<EOF
[Unit]
Description=Set IRQ CPU Affinity for ${IFACE}
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/set-irq-affinity.sh ${IFACE} ${IRQ_CPUS}
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now irq-affinity.service
echo "[OK] Done. Please bind your game processes away from ${IRQ_CPUS}."
重载完 irq-affinity.service 的那秒,/proc/interrupts 里每条 TxRx-N 都乖乖待在我分配的 CPU 上。ksoftirqd 的波峰被削平,softnet_stat 的丢弃从“跳着跑”变成了慢速递增的直线;随后 3 分钟内,战团战的掉线告警曲线像被人按住了头。凌晨 2:05,坐在机房的地板上,我跟同事说了一句:“这活儿,还是得和内核算账。”
后来我们把这套IRQ CPU 亲和 + 队列均衡 + 软中断隔离的策略固化成脚本、接入 CM,香港 CN2 节点的 p99 延迟再也没见过当晚那样的尖刺。你要问我最扎心的教训?别把“公网延迟”当背锅侠。很多时候,玩家在家里看到的卡顿,原因就卡在你机柜里那几个核上。
如果你也在做延迟敏感的网游或实时服务,不妨照着做一遍。有什么现场异状或驱动差异,直接把你的 lscpu -e、/proc/interrupts 和 ethtool -S 截给我——我会按你机器的核拓扑,帮你把 IRQ 和 RPS 掩码量身裁剪。