4、中断亲和性与负载均衡:CPU中断亲和性设置、多核中断分发、中断负载均衡算法

好,我们接着聊中断。前面几章我们把中断的整个生命周期、优先级、嵌套都捋了一遍。这一章,咱们得聊聊中断怎么跟多核CPU打交道。

你想想看,现在的服务器动不动就几十上百个核。如果所有中断都往一个核上怼,那这个核不得累死?其他核在旁边看热闹?这显然不行。所以就有了中断亲和性与负载均衡这套东西。

4.1 中断亲和性:把中断绑定到指定CPU

中断亲和性,说白了就是告诉硬件:「这个中断,你只许发给这几个CPU核」。我个人习惯把这个叫做「中断的CPU绑核」。

为什么要做这个?原因其实很朴素:

  • 缓存局部性:如果一个设备的中断总是发给同一个核,那这个核的L1/L2 cache里很可能就缓存了该设备相关的数据结构。换到另一个核,cache miss就来了。
  • 减少核间干扰:有些核跑的是延迟敏感的业务,比如音频处理。你一个网卡中断噼里啪啦地打过来,音频就卡顿了。
  • NUMA亲和性:在NUMA架构下,中断最好发给跟设备在同一NUMA节点上的核。跨节点访问内存,那延迟可不是闹着玩的。

在Linux里,设置中断亲和性主要通过/proc/irq/{IRQ_NUM}/smp_affinity这个文件。它里面是一个十六进制掩码,每一位代表一个CPU核。

# 查看中断号48的当前亲和性
cat /proc/irq/48/smp_affinity
# 输出可能是:00000000,000000ff
# 这表示中断可以发给CPU0-7

# 设置只发给CPU0(位掩码为0x01)
echo 1 > /proc/irq/48/smp_affinity

# 设置只发给CPU2(位掩码为0x04)
echo 4 > /proc/irq/48/smp_affinity

小技巧:我习惯用cat /proc/interrupts先看看每个中断在各个核上的分布情况。如果发现某个中断在多个核上都有大量计数,那说明它可能没有被正确绑定,或者驱动做了某种分发。

嗯,这里要注意:smp_affinity文件里的掩码是十六进制,但它的位数跟CPU数量有关。32核以下的机器,一个32位整数就够了。超过32核,就得用逗号分隔的多个32位段。比如00000000,00000001表示只发给CPU0。

4.2 多核中断分发:硬件怎么把中断送到指定核

中断亲和性设置好了,那硬件是怎么知道要把中断发给哪个核的呢?这就要看中断控制器了。

在x86平台上,现代的中断控制器是APIC(高级可编程中断控制器)。每个CPU核都有一个Local APIC,整个系统还有一个I/O APIC。I/O APIC负责接收设备的中断请求,然后根据配置,把中断消息发送给指定CPU的Local APIC。

具体流程是这样的:

  1. 设备产生中断请求,发送给I/O APIC。
  2. I/O APIC查表,找到这个中断对应的重定向表项(Redirection Table Entry)
  3. 重定向表项里包含了目标CPU的APIC ID、中断向量号、触发模式等信息。
  4. I/O APIC把中断消息通过系统总线发送给目标CPU的Local APIC。
  5. Local APIC收到后,如果该CPU的IF位(中断标志位)为1,就触发中断。

这里有个关键点:重定向表项里的目标CPU可以是一个,也可以是多个。如果设置成多个,那就是我们常说的「中断分发」模式。硬件会按照某种策略(比如轮询、最低优先级)选择一个CPU来响应。

核心概念:中断分发不是软件做的,是硬件I/O APIC做的。软件只需要配置好重定向表项,硬件就会自动把中断送到指定CPU。这也是为什么中断亲和性设置能生效的根本原因。

我记得有一次调试一个网卡驱动,发现中断总是集中在CPU0上。我明明设置了亲和性到CPU2,但/proc/interrupts显示CPU0的计数还在涨。后来查了半天,发现是BIOS里把该PCIe设备的MSI-X中断重定向给锁死了。嗯,硬件层面的坑,有时候比软件还难搞。

4.3 中断负载均衡算法:让中断分布更均匀

手动设置亲和性,适合那些你明确知道该绑哪个核的场景。但更多时候,我们希望系统能自动把中断负载均衡到各个CPU上。这就是中断负载均衡算法要做的事。

Linux内核里有一个守护进程叫irqbalance,它就是干这个活的。它会定期(默认每10秒)检查各个CPU的中断负载情况,然后动态调整中断的亲和性。

irqbalance的核心算法大致是这样的:

  1. 收集数据:读取/proc/interrupts/proc/stat,获取每个中断的计数和每个CPU的使用率。
  2. 计算负载:对每个中断,计算它在每个CPU上的中断率(每秒中断次数)。
  3. 评估不平衡度:计算所有CPU的中断负载方差。如果方差超过阈值,就认为需要重新平衡。
  4. 选择迁移目标:找到负载最轻的CPU,把高负载的中断迁移过去。
  5. 执行迁移:写入/proc/irq/{IRQ_NUM}/smp_affinity,改变亲和性。

但这里有个问题:irqbalance默认是不考虑中断类型的。它把网卡中断、磁盘中断、定时器中断都一视同仁。这在某些场景下会出问题。

避坑指南:我曾经在一个高负载的数据库服务器上,发现irqbalance把网卡中断从CPU0迁移到了CPU2。结果CPU2上跑着关键的数据库线程,网卡中断一上去,数据库的延迟直接飙升了30%。从那以后,我对关键业务的中断都是手动绑核,关掉irqbalance对它们的干预。

怎么关?很简单:

# 停止irqbalance服务
systemctl stop irqbalance
systemctl disable irqbalance

# 或者,只对特定中断排除
# 在/etc/irqbalance/irqbalance.conf中配置
# 比如:banirq=48,49,50

4.4 实战:手动绑核 vs 自动均衡

说了这么多,到底什么时候用手动绑核,什么时候用自动均衡?我个人的经验是这样的:

场景 推荐方式 原因
网卡中断(高吞吐) 手动绑核,配合RPS/RFS 网卡中断量大,需要精细控制
磁盘中断(NVMe) 手动绑核到NUMA节点内 避免跨NUMA访问
普通设备中断 自动均衡(irqbalance) 省心,负载均匀
虚拟化场景 手动绑核,隔离物理核 避免vCPU被中断抢占
实时系统 手动绑核到隔离核 保证确定性延迟

另外,现代网卡大多支持多队列(Multi-Queue)。每个队列可以有自己的中断号。这时候,你可以把每个队列的中断分别绑定到不同的CPU上。比如一个4队列的网卡,把队列0-3分别绑到CPU0-3。这样每个CPU处理自己的队列,互不干扰。

# 假设网卡eth0有4个队列,中断号分别是48,49,50,51
echo 1 > /proc/irq/48/smp_affinity   # 队列0 -> CPU0
echo 2 > /proc/irq/49/smp_affinity   # 队列1 -> CPU1
echo 4 > /proc/irq/50/smp_affinity   # 队列2 -> CPU2
echo 8 > /proc/irq/51/smp_affinity   # 队列3 -> CPU3

嗯,这里要注意:echo 2表示二进制00000010,对应CPU1(从0开始计数)。别搞反了,我刚开始就犯过这个错,把中断全绑到了CPU0上。

4.5 总结一下

中断亲和性与负载均衡,说白了就是解决「中断该给谁处理」的问题。手动绑核适合精细控制,自动均衡适合通用场景。但不管哪种方式,核心都是要理解硬件的分发机制和软件的可配置接口。

我个人建议:先手动,后自动。先手动绑核跑一段时间,摸清中断的规律和业务的影响。如果发现手动绑核太麻烦或者负载变化大,再考虑引入irqbalance。但一定要监控好,别让自动均衡把你的关键业务给「均衡」崩了。

下一章,我们会聊聊更高级的中断优化技术——中断合并和中断节流。这些东西在高速网络场景下特别有用,到时候再细说。