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

香港服务器运行 CentOS 7 时,我是如何调优 irqbalance 与 CPU 亲和性,把高流量业务的中断延迟打下来的

发布人:Minchunlin 发布时间:2025-08-28 09:41 阅读量:718


凌晨 2 点,香港荃湾机房 6 楼,我们的跨境电商做秒杀活动,香港集群是北向入口,峰值单机 11–12 Gbps。NOC 一直在吼网络抖动,监控里 p99 RTT 竖了根“钉子”。我带着阿杰蹲在第 3 排机柜前,抱着保温杯里已经凉透的咖啡,一边看 top 里几个 ksoftirqd 飙到 100%,一边想:这不是应用问题,是中断被打散了、软中断没跑在对的核上。

那一晚,我把 irqbalance“管好”、把中断“拴住”、把应用“让开”,最后 RTT“钉子”倒了。

一、环境与目标

硬件参数(单台样例)

机型:Supermicro SYS-1029U(1U)

CPU:2 × Intel Xeon Silver 4210(10C20T ×2,NUMA=2)

内存:768 GB DDR4-2666

网卡:Intel X710-DA4(四口 10GbE,驱动 i40e,MSI-X 中断,支持 RSS/RPS/XPS)

系统盘:Samsung PM983 NVMe 3.84 TB

交换机:TOR 为 25G 下行,10G 上联分光

软件栈

  • OS:CentOS 7.9(内核 3.10.0-1160.el7.x86_64)
  • 驱动:i40e 2.12.x(随内核)
  • irqbalance:v1.0.7-12.el7

应用:Nginx + 自研网关(epoll),容器化(Docker,cgroup v1)

目标

  • 让网卡队列的硬中断与软中断亲和在同一 NUMA 节点、成对的超线程兄弟核上;
  • 让 irqbalance 只在“保洁核”上打扫,不要碰应用核;
  • 降低中断抖动,压 p99/p999 RTT,消灭 ksoftirqd/N 漏斗。

二、快速原理图(为什么要这么搞)

每个队列(RX/TX)→ 绑定一个或多个 MSI-X IRQ。

硬中断(hardirq)醒来后唤起 软中断(softirq)在 ksoftirqd/N 上干活(N 为 CPU id),如果软中断跑到不在同一缓存域/NUMA 的核上,来回穿越会抖。

irqbalance 默认“平均主义”,会把 IRQ 在所有可用 CPU 上漂移,但平均≠稳定;高频网络 IRQ 更需要就地消化。

亲和性要对齐三件事:NUMA、队列、超线程兄弟。另外再配 RPS/XPS 让软中断/发送路径也一致。

三、现状排查(我在现场怎么判断“它抖了”)

# 看软中断占比、ksoftirqd 是否打满
top -H -p $(pidof ksoftirqd/*)   # 或者直接 top,观察 ksoftirqd/0, ksoftirqd/7 等
mpstat -P ALL 1 5

# 看中断分布是否倾斜、是否频繁漂移
grep -E "i40e|eth1" /proc/interrupts | sort -nk2

# 看网卡队列、RSS/XPS 能力
ethtool -l eth1       # 当前/最大队列数
ethtool -g eth1       # ring 大小
ethtool -S eth1 | egrep "dropped|missed|errors|no_desc"

问题征兆(真实摘录样式):

  • ksoftirqd/3、ksoftirqd/7 常年 80–100%;
  • /proc/interrupts 中与 eth1-TxRx-* 相关 IRQ 大量堆在 CPU 3/7,且过几分钟会换一批核(被 irqbalance 摇摆);
  • ethtool -S eth1 有少量 rx_no_desc(说明 RX ring 被打爆过),同时 busy_poll 未开。

四、调优设计(我的“布阵”)

队列与 NUMA 对齐:

NIC eth1 物理上挂在 NUMA 0(用 cat /sys/class/net/eth1/device/numa_node 验证),我把它的 8 条 RX/TX 队列和对应 IRQ 都绑在 NUMA 0 的核上:CPU 0-9,20-29 里挑 8 对兄弟核。

核角色划分(以 20 物理核/40 逻辑核为例):

  • 中断+软中断核(Housekeeping-IRQ):CPU 0,1,2,3,4,20,21,22,23(NUMA0 的 5 对超线程各取一条线)
  • 应用核(App):CPU 8-15,28-35(NUMA0 近内存 + 远一点的也可以)
  • 后台/系统核:其余(NUMA1 为其他口或系统任务)

策略:

  • 限制 irqbalance 活动范围:禁止它碰应用核,只让它在“保洁核”上活动(或者干脆停掉,完全手排)。
  • 固定 IRQ 亲和性:对每个 eth1-TxRx-N 指定一个 CPU(或一对兄弟),使用 smp_affinity_list(可读的 0,2,4 形式)。
  • RPS/XPS 对齐:rps_cpus/xps_cpus 与硬中断落点一致,防止软中断在别处醒。
  • 轻度中断合并:ethtool -C rx-usecs 8 rx-frames 0,在 10G–12G 下小幅合并,不要太激进以免引入抖动。
  • 队列到位:确保 ethtool -L eth1 combined 8,RSS 足够分散。

五、动手步骤(逐条命令,能复制就能复现)

1)确认 NUMA 与 CPU 拓扑

numactl -H
lscpu -e=CPU,CORE,SOCKET,NODE,ONLINE,CONFIGURED
cat /sys/class/net/eth1/device/numa_node   # 返回 0 表示在 NUMA 0

经验:优先选择同 NUMA 的核,并且尽量避开兄弟线程对(一个给 IRQ/softirq,另一个可给轻量任务或空着)。我常用“同 core 的两个 thread 不同时打满”。

2)给网卡开足队列

ethtool -l eth1
# Current hardware settings: RX: 8 TX: 8 Combined: 8
# Maximum hardware settings: RX: 8 TX: 8 Combined: 8

# 若 Current < Maximum:
ethtool -L eth1 combined 8

3)先“关”或“圈起来” irqbalance

方案 A(限制活动范围):

# 仅允许 irqbalance 在 CPU 0-5,20-25 上活动,屏蔽应用核(例)
# CentOS 7 使用 /etc/sysconfig/irqbalance
vi /etc/sysconfig/irqbalance
# 添加或修改:
# 计算掩码的简便做法:尽量用 smp_affinity_list(见下一步),
# 但这里需要16进制:屏蔽应用核,如 8-15,28-35 -> 0x000000ff00ff00(示例)
IRQBALANCE_BANNED_CPUS=0x000000ff00ff00

systemctl restart irqbalance
systemctl status irqbalance

方案 B(手排就停):

systemctl stop irqbalance
systemctl disable irqbalance
systemctl mask irqbalance

建议:如果你要精确绑每个队列到具体核,就采用方案 B;如果你希望快速稳住局面,先用方案 A 把应用核“让开”。

4)为每个 IRQ 指定 smp_affinity_list

找出 eth1 的 IRQ:

grep -E "eth1|i40e" /proc/interrupts
# 例如:
# 141: ... i40e-eth1-TxRx-0
# 142: ... i40e-eth1-TxRx-1
# ...
# 148: ... i40e-eth1-TxRx-7

规划映射(示例,全部落在 NUMA0):

队列 IRQ 目标 CPU(亲和)
TxRx-0 141 0
TxRx-1 142 2
TxRx-2 143 4
TxRx-3 144 6
TxRx-4 145 20
TxRx-5 146 22
TxRx-6 147 24
TxRx-7 148 26

设置命令(使用列表,比十六进制直观):

for n in 0 1 2 3 4 5 6 7; do
  irq=$(grep -E "i40e-eth1-TxRx-${n}\b" /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')
  case "$n" in
    0) cpu=0 ;;
    1) cpu=2 ;;
    2) cpu=4 ;;
    3) cpu=6 ;;
    4) cpu=20 ;;
    5) cpu=22 ;;
    6) cpu=24 ;;
    7) cpu=26 ;;
  esac
  echo "$cpu" > /proc/irq/${irq}/smp_affinity_list
  echo "IRQ $irq -> CPU $cpu"
done

验证:

for irq in $(grep -E "i40e-eth1-TxRx-" /proc/interrupts | awk -F: '{print $1}'); do
  printf "IRQ %s : " $irq; cat /proc/irq/$irq/smp_affinity_list
done

说明:smp_affinity_list 支持 0-3,20-23 这种写法;比 smp_affinity 的十六进制友好多了。

5)让软中断路径(RPS/XPS)也跟着走

# 把 RPS 亲和设置为与硬中断相同的 CPU(示例以队列0落在 CPU0)
# rps_cpus 接受位图或列表(CentOS 7 支持列表)
for q in /sys/class/net/eth1/queues/rx-*; do
  idx=$(basename $q | cut -d- -f2)
  case "$idx" in
    0) cpus="0" ;;
    1) cpus="2" ;;
    2) cpus="4" ;;
    3) cpus="6" ;;
    4) cpus="20" ;;
    5) cpus="22" ;;
    6) cpus="24" ;;
    7) cpus="26" ;;
  esac
  echo $cpus > $q/rps_cpus
done

# XPS 让发送路径也在同核
for q in /sys/class/net/eth1/queues/tx-*; do
  idx=$(basename $q | cut -d- -f2)
  case "$idx" in
    0) cpus="0" ;;
    1) cpus="2" ;;
    2) cpus="4" ;;
    3) cpus="6" ;;
    4) cpus="20" ;;
    5) cpus="22" ;;
    6) cpus="24" ;;
    7) cpus="26" ;;
  esac
  echo $cpus > $q/xps_cpus
done

6)适度的中断合并(coalescing)

# 初值保守一点
ethtool -C eth1 rx-usecs 8 rx-frames 0 tx-usecs 4
# 观察 p99 RTT 与 CPU 利用率,别把 usecs 拉太大

7)应用与系统进程绑核(让出 IRQ 核)

systemd 方式(推荐):

# /etc/systemd/system/gateway.service.d/cpu.conf
[Service]
CPUAffinity=8-15 28-35

systemctl daemon-reload
systemctl restart gateway

临时验证:

taskset -c 8-15,28-35 <your_cmd>

8)持久化脚本(避免重启后丢失)

创建系统服务,在网卡 up 后设置亲和与 RPS/XPS:

# /usr/local/sbin/pin_eth1_irqs.sh
#!/bin/bash
set -e
IF=eth1
declare -A map=( [0]=0 [1]=2 [2]=4 [3]=6 [4]=20 [5]=22 [6]=24 [7]=26 )
# IRQ 亲和
for n in "${!map[@]}"; do
  irq=$(grep -E "i40e-${IF}-TxRx-${n}\b" /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')
  [ -n "$irq" ] && echo "${map[$n]}" > /proc/irq/${irq}/smp_affinity_list
done
# RPS/XPS
for dir in rx tx; do
  for q in /sys/class/net/${IF}/queues/${dir}-*; do
    idx=$(basename $q | cut -d- -f2)
    cpu="${map[$idx]}"
    [ -n "$cpu" ] && echo "$cpu" > $q/${dir/rx/rps}_cpus 2>/dev/null || true
    [ -n "$cpu" ] && echo "$cpu" > $q/${dir/tx/xps}_cpus 2>/dev/null || true
  done
done

chmod +x /usr/local/sbin/pin_eth1_irqs.sh

# /etc/systemd/system/pin-eth1-irqs.service
[Unit]
Description=Pin eth1 IRQs and set RPS/XPS
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/pin_eth1_irqs.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

systemctl daemon-reload
systemctl enable --now pin-eth1-irqs.service

六、验证与量化(“抖没抖”,数据说话)

压测前后 10 分钟对比(单机,12 Gbps,payload 混合)

指标 调优前 调优后
p50 RTT(µs) 420 260
p99 RTT(µs) 2100 780
p999 RTT(µs) 5800 1850
ksoftirqd/* 最高占用 100% ≤ 45%
softirq time(mpstat) 32% 11%
ethtool -S eth1 rx_no_desc 2–5/min 0–1/10min
网卡队列偏斜(最大队列/最小队列 IRQ 计数比) 3.8× 1.2×

命令核对:

# 中断是否稳定分布
watch -n 1 'grep -E "i40e-eth1-TxRx-" /proc/interrupts'

# 软中断是否下降
mpstat -P ALL 1 10 | egrep -A1 "CPU|soft"

# 应用核是否避让成功
ps -Leo pid,psr,comm,args | grep gateway | head

七、那些你会踩的坑(都是我踩过的)

IRQ 号会变:驱动加载顺序不同、重启后 141 可能变 149。
→ 用 正则匹配队列名(上面的脚本就是),别把 IRQ 号写死。

irqbalance 会“打回原形”:
你手动绑完,过一会儿又散了,多半是它还在跑。
→ 要么 mask 掉 irqbalance,要么在它上面圈出禁区(IRQBALANCE_BANNED_CPUS)。

超线程兄弟核:
把硬/软中断和应用都挤在同一个 core 的两个 thread,会出现互相抢资源。
→ 同 core 只放一种重活,另一条线给轻活或空着。

容器与 cgroup:
只在容器里 taskset 没用,宿主的 IRQ 亲和还在外面。
→ 宿主层面先排好,再用 systemd 的 CPUAffinity 或 docker 的 --cpuset-cpus 管应用。

虚拟化的 vhost-net/virtio:
如果是 KVM,vhost-net 的中断也要绑;不然中断在宿主 NUMA0,VM vCPU 在 NUMA1,来回跑死。

SR-IOV/DPDK 场景:
直通或用户态收包不走内核栈,这一套要换打法(绑 poll 线程、绑 PMD)。别把两种方案混着配。

BIOS 电源策略:
C-State 太深会加大唤醒延迟。
→ BIOS 里关深 C-State,Linux 加 intel_pstate=disable 或 processor.max_cstate=1 要谨慎评估能耗。

队列数与会话数不匹配:
RSS 8 队列,结果五成流量都射到 TxRx-0。
→ ethtool -n eth1 rx-flow-hash tcp4 看哈希键,必要时 调 RSS key / indirection table(ethtool -X),让会话更均匀。

八、可复制的“最小闭环”清单(照抄也能跑)

ethtool -L eth1 combined 8

选择 同 NUMA 的 8 个 CPU:0,2,4,6,20,22,24,26

停 irqbalance 或设置 IRQBALANCE_BANNED_CPUS 屏蔽应用核

给 eth1-TxRx-[0..7] 分别写 smp_affinity_list(如 0/2/4/...)

给 rx-*/rps_cpus、tx-*/xps_cpus 写同样列表

轻度 ethtool -C eth1 rx-usecs 8

应用 CPUAffinity=8-15,28-35

写 systemd oneshot 持久化

九、附录:几个小工具片段

A. 把 0-3,8-9 这种列表转 16 进制掩码(给 IRQBALANCE_BANNED_CPUS 用)

如果你非要算十六进制,这个 bash 足够用(但更推荐用 *_affinity_list)

cpulist_to_hex() {
  local IFS=',' part a b i
  local -A set=()
  for part in $1; do
    if [[ $part == *-* ]]; then a=${part%-*}; b=${part#*-}; for ((i=a;i<=b;i++)); do set[$i]=1; done
    else set[$part]=1; fi
  done
  local max=0; for i in "${!set[@]}"; do (( i>max )) && max=$i; done
  local chunk=0 out=()
  for ((i=0;i<=max;i++)); do
    (( set[$i] )) && (( chunk |= 1<<(i%32) ))
    if (( i%32==31 )); then out=($(printf "%08x" $chunk) "${out[@]}"); chunk=0; fi
  done
  if (( max%32!=31 )); then out=($(printf "%08x" $chunk) "${out[@]}"); fi
  echo "0x$(printf "%s" "${out[@]}" | sed 's/^0\+//')"
}
# 示例: cpulist_to_hex 8-15,28-35  => 0x000000ff00ff00

B. 一键打印 NIC 队列与 IRQ → CPU 映射
print_irqs() {
  IF=$1
  printf "%-10s %-6s %-10s\n" "Queue" "IRQ" "CPU"
  for n in $(ls -1 /sys/class/net/${IF}/queues/rx-* | awk -F- '{print $NF}'); do
    irq=$(grep -E "${IF}-.*TxRx-${n}\b" /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')
    cpu=$(cat /proc/irq/${irq}/smp_affinity_list 2>/dev/null || echo "-")
    printf "%-10s %-6s %-10s\n" "RxTx-$n" "$irq" "$cpu"
  done
}
print_irqs eth1

十、收尾:那一晚之后

第二天清晨 5 点,NOC 的告警面板像被人顺手抹了一遍,p99 RTT 从两位数毫秒落回了亚毫秒级。我们把仪表盘截了图发在群里,阿杰说:“原来把中断拴住,世界就安静了。”我笑着摇杯子,冰凉的咖啡在杯壁上慢慢滑下去。

之后的每次大促,我们都先按这套“布阵表”排好——先决定谁来收包(IRQ 核),再让应用让路(App 核),最后把软中断与发送路径对齐(RPS/XPS)。机房的风还是呼呼地吹,但我们心里不再抖了。

结语要点(给赶时间的你)

  • irqbalance 不是洪水猛兽,但一定要圈地(BANNED_CPUS),或停掉配合手排。
  • smp_affinity_list 比十六进制直观,用它给每个队列定点落地。
  • RPS/XPS 与硬中断一致,NUMA 就地消化。
  • 少量 coalescing 折中吞吐与延迟;观测永远比拍脑袋重要。
目录结构
全文