上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-22 12:09 阅读量:747


夜里 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 掩码量身裁剪。

目录结构
全文