概述

线上跑着一个高频交易系统,P99 延迟平时 2ms,但偶尔飙到 20ms。CPU 使用率不高,内存充足,网络正常。查了一圈,发现是 CPU 调度器把关键线程踢到了另一个核上,L3 缓存全部 miss,延迟直接翻了 10 倍。

这种问题不是靠加资源能解决的。问题出在"共享"——所有进程共享 CPU 核心,调度器自由分配,谁也不知道哪个线程是延迟敏感的。

解决办法就是 CPU 隔离:把关键业务绑到专属核心上,不让别的进程碰。同时做 NUMA 调优,让 CPU 和内存在物理上"就近",避免跨节点访问带来的延迟翻倍。

这篇笔记覆盖从基础概念到生产实操的完整链路:isolcpus 内核参数、cpuset cgroup、taskset 绑核、NUMA 亲和性、中断绑核、以及组合使用的最佳实践。所有命令都在 Ubuntu 22.04(内核 5.15)和 CentOS 8 上实测过。

基础概念:为什么要隔离 CPU

CPU 调度器是怎么工作的

Linux 默认使用 CFS(Completely Fair Scheduler,完全公平调度器)分配 CPU 时间。CFS 的设计目标是"公平"——每个进程根据权重获得 CPU 时间片,调度器在所有可用核心之间自由迁移进程。

听起来没问题,但对延迟敏感的场景是个灾难:

  1. 上下文切换开销:线程从 CPU A 迁移到 CPU B,L1/L2 缓存全部失效,需要重新从内存加载数据。一次迁移的代价是微秒级的延迟抖动。
  2. 缓存污染:其他进程跑在你的目标核上,把你之前缓存的数据挤出去,下次你的线程回来时全是 cache miss。
  3. 中断干扰:网卡中断、定时器中断随时打断你的线程。对于要求微秒级响应的系统,一次中断就是一次延迟尖峰。

NUMA 架构是什么

NUMA(Non-Uniform Memory Access,非统一内存访问)是多路服务器的标配架构。简单说就是:每个 CPU 插槽有自己的本地内存,访问自己的内存很快,访问别的 CPU 的内存要跨 QPI/UPI 总线,延迟翻倍。

打个比方:NUMA 就像两栋办公楼的格局。你在一号楼办公,资料柜也在一号楼,拿东西很快。但如果要拿二号楼的资料,得穿过走廊去拿,明显慢一拍。

一台典型的双路服务器:

NUMA Node 0 (CPU 0-31, 内存 0-128GB)
  └── 本地内存访问延迟: ~80ns

NUMA Node 1 (CPU 32-63, 内存 128-256GB)
  └── 本地内存访问延迟: ~80ns

跨节点访问延迟: ~140ns  ← 差了将近一倍

numactl --hardware 查看你的 NUMA 拓扑:

$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 0 size: 128768 MB
node 0 free: 89012 MB
node 1 cpus: 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
node 1 size: 129020 MB
node 1 free: 91045 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

注意 node distances:本地距离是 10,跨节点是 21。距离值是相对值,代表访问延迟的比例。21 大约是 10 的两倍多——跨节点访问确实慢得多。

numastat 查看各节点的内存分配情况:

$ numastat
                           node 0           node 1
numa_hit              89342156         67823451
numa_miss               234561          123456
numa_foreign             12345           23456
interleave_hit          456789          345678
local_node            89012345         67012345
other_node             345611           823456
  • numa_hit:本节点分配命中数,越高越好
  • numa_miss:本节点分配未命中(本该在别的节点),越低越好
  • other_node:跨节点访问次数,这个数字持续增长说明 NUMA 策略有问题

方法一:isolcpus 内核参数(启动级隔离)

什么是 isolcpus

isolcpus 是 Linux 内核的启动参数,用于在系统启动时就把指定 CPU 核心从全局调度器中隔离出来。被隔离的核不参与普通进程调度,只有手动绑定的进程才能在上面运行。

这是最强的隔离方式——从内核启动那一刻就生效,不依赖运行时配置。

配置方法

编辑 GRUB 配置:

# 编辑 /etc/default/grub
sudo vim /etc/default/grub

# 在 GRUB_CMDLINE_LINUX 中添加 isolcpus 参数
# 例如隔离 CPU 4-7(4个核)
GRUB_CMDLINE_LINUX="isolcpus=4-7"

# 更新 GRUB
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
# Ubuntu 用这个:
# sudo update-grub

# 重启生效
sudo reboot

重启后验证:

$ cat /proc/cmdline | grep isolcpus
BOOT_IMAGE=/boot/vmlinuz-5.15.0-91-generic root=... isolcpus=4-7

isolcpus 的高级选项

isolcpus 不只是简单地隔离 CPU,它还有几个子选项可以配合使用:

# 完整配置示例
GRUB_CMDLINE_LINUX="isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7"
参数作用适用场景
isolcpus=4-7从全局调度器中隔离 CPU 4-7基础隔离
nohz_full=4-7禁用隔离核上的定时器中断低延迟场景,减少中断抖动
rcu_nocbs=4-7RCU 回调不在隔离核上执行进一步减少内核干扰
isolcpus=4-7,domain将隔离核排除在负载均衡域外默认已启用,显式声明
isolcpus=4-7,managed_irq限制中断亲和性减少硬件中断干扰

三个参数配合使用,效果最佳。nohz_full 减少定时器中断,rcu_nocbs 把 RCU 回调移走,isolcpus 阻止普通进程调度——三层隔离叠加,隔离核上基本只剩下你手动绑定的进程。

isolcpus 的局限

isolcpus 不是万能的:

  1. 需要重启:修改后必须重启系统才能生效,不适合动态调整
  2. 不完全隔离:内核线程和某些中断仍然可能在隔离核上运行,需要手动迁移
  3. 与 cgroup cpuset 的优先级问题:如果同时配置了 isolcpus 和 cpuset cgroup,内核优先遵守 isolcpus 的限制。(别再乱配 isolcpus 了 中有详细分析)

方法二:cpuset cgroup(运行时隔离)

为什么用 cpuset 而不是 isolcpus

isolcpus 需要重启,不够灵活。cpuset 是 cgroup 的一个子系统,可以在运行时动态创建和修改,不需要重启系统。适合需要频繁调整的场景。

cgroup v1 配置方法

# 挂载 cpuset 子系统(如果尚未挂载)
sudo mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset

# 创建一个隔离组
sudo mkdir /sys/fs/cgroup/cpuset/critical_app

# 分配 CPU 核 4-5 给这个组
sudo echo "4-5" > /sys/fs/cgroup/cpuset/critical_app/cpuset.cpus

# 分配 NUMA 节点 0 的内存
sudo echo "0" > /sys/fs/cgroup/cpuset/critical_app/cpuset.mems

# 开启独占模式(关键!其他 cpuset 不能使用这些 CPU)
sudo echo "1" > /sys/fs/cgroup/cpuset/critical_app/cpuset.cpu_exclusive

# 把进程加入这个 cpuset
sudo echo <PID> > /sys/fs/cgroup/cpuset/critical_app/tasks

cpu_exclusive 这个选项很关键。设为 1 后,这些 CPU 核心就变成了这个 cpuset 组的"私产",其他 cpuset 组不能使用。真正做到了独占。

cgroup v2 配置方法

现代 Linux 发行版(内核 5.15+)默认使用 cgroup v2。配置方式略有不同:

# 启用 cpuset 控制器
sudo echo "+cpuset" > /sys/fs/cgroup/cgroup.subtree_control

# 创建子组
sudo mkdir /sys/fs/cgroup/critical_app

# 分配 CPU 核
sudo echo "4-5" > /sys/fs/cgroup/critical_app/cpuset.cpus

# 分配 NUMA 节点
sudo echo "0" > /sys/fs/cgroup/critical_app/cpuset.mems

# 把进程加入
sudo echo <PID> > /sys/fs/cgroup/critical_app/cgroup.procs

cgroup v2 的语法更简洁,但注意 v2 中没有 cpu_exclusive 选项,独占隔离需要配合 isolcpus 来实现。

用 systemd 管理服务级 cpuset

生产环境推荐用 systemd 来管理 cpuset,比手动操作 cgroup 文件更可靠:

# /etc/systemd/system/critical-app.service
[Unit]
Description=Critical Low-Latency Application
After=network.target

[Service]
ExecStart=/opt/critical-app/bin/server
# 绑定到 CPU 4-5
CPUAffinity=4,5
# 设置 NUMA 亲和性
NUMAPolicy=bind
NUMAMask=0
# 限制内存节点
AllowedCPUs=4-5
# 重启策略
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start critical-app

systemd 的好处是:服务重启后自动恢复 cpuset 配置,不需要手动维护。

方法三:taskset(快速绑核)

taskset 的使用

taskset 是最简单的绑核工具,适合临时验证或快速部署:

# 启动时绑定到 CPU 4
taskset -c 4 ./my_application

# 绑定到 CPU 4 和 5
taskset -c 4,5 ./my_application

# 查看运行中进程的 CPU 亲和性
taskset -cp <PID>

# 修改运行中进程的 CPU 亲和性
taskset -cp 4 <PID>

taskset 的局限是:它只管进程亲和性,不阻止其他进程也使用这些 CPU 核。如果只是用 taskset 绑核而没有做 isolcpus 隔离,其他进程仍然可以调度到同一个核上,只是你的进程被"建议"在这些核上跑而已。

编程方式绑核

在 C/C++ 程序中直接绑核:

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <pthread.h>

int main() {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(4, &cpuset);  // 绑定到 CPU 4

    // 绑定当前线程
    if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) != 0) {
        perror("sched_setaffinity failed");
        return 1;
    }

    printf("Pinned to CPU 4\n");
    // 你的业务逻辑...
    while (1) { /* work */ }
    return 0;
}

多线程程序中给特定线程绑核:

void* worker_thread(void* arg) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(5, &cpuset);  // 这个线程绑到 CPU 5

    pthread_t current = pthread_self();
    if (pthread_setaffinity_np(current, sizeof(cpu_set_t), &cpuset) != 0) {
        perror("pthread_setaffinity_np failed");
    }

    // 线程业务逻辑...
    while (1) { /* work */ }
    return NULL;
}

验证线程实际运行在哪个 CPU 上:

# 查看进程所有线程当前跑在哪个 CPU
ps -eLo pid,tid,psr,comm | grep my_application

# psr 列就是当前处理器编号

NUMA 亲和性调优

numactl 工具

numactl 是控制 NUMA 策略的核心工具:

# 查看 NUMA 拓扑
numactl --hardware

# 查看 NUMA 统计
numastat

# 在 NUMA 节点 0 上运行程序(CPU 和内存都绑定到节点 0)
numactl --cpunodebind=0 --membind=0 ./my_application

# 简写
numactl -N 0 -m 0 ./my_application

# 只绑定 CPU 节点,内存自由分配
numactl --cpunodebind=0 ./my_application

# 内存交错分配(跨节点均衡)
numactl --interleave=all ./my_application

NUMA 策略选择

策略命令适用场景效果
绑定(bind)numactl -N 0 -m 0 ./appCPU 密集型,延迟敏感CPU 和内存都在同一节点,最优延迟
交错(interleave)numactl --interleave=all ./app内存密集型,大内存应用内存带宽翻倍,但延迟波动
首选(preferred)numactl --preferred=0 ./app优先本地,不够再借尽量本地分配,本地不够时跨节点

数据库的 NUMA 配置

数据库是 NUMA 调优的重灾区。MySQL、PostgreSQL 默认不感知 NUMA,容易出现"跨节点内存访问"导致性能下降。

MySQL NUMA 配置

# 启动 MySQL 时绑定到 NUMA 节点 0
numactl --interleave=all mysqld_safe &

# 或者用 numactl 绑定到特定节点
numactl --cpunodebind=0 --membind=0 mysqld_safe &

PostgreSQL NUMA 配置

# 在 postgresql.conf 中设置
# 使用 huge pages 减少 TLB miss
huge_pages = on

# 启动时绑定
numactl --interleave=all pg_ctl start

实测数据(PostgreSQL 15,64GB 内存,双路 E5-2680 v4):

配置QPSP99 延迟说明
默认(无 NUMA 配置)12,0008ms内存跨节点访问严重
interleave 模式18,0005ms内存均衡分布,带宽提升
bind 模式(单节点)15,0003ms延迟最优,但带宽受限

结论很明确:要带宽用 interleave,要延迟用 bind。根据业务场景选。

中断绑核

为什么要绑中断

网卡中断默认由所有 CPU 分担(通过 irqbalance 守护进程动态分配)。对于做了 CPU 隔离的系统,隔离核上不应该有任何中断干扰。

关闭 irqbalance

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

手动绑定中断

# 查看网卡中断号
$ cat /proc/interrupts | grep eth0
  28:   123456   234567   345678   456789   0   0   0   0   PCI-MSI 1572864-edge   eth0-TxRx-0
  29:   234567   345678   456789   567890   0   0   0   0   PCI-MSI 1572865-edge   eth0-TxRx-1
  30:   345678   456789   567890   678901   0   0   0   0   PCI-MSI 1572866-edge   eth0-TxRx-2
  31:   456789   567890   678901   789012   0   0   0   0   PCI-MSI 1572867-edge   eth0-TxRx-3

# 查看当前中断亲和性(位掩码)
$ cat /proc/irq/28/smp_affinity
00000000,00000000,00000000,000000ff
# ff 表示 CPU 0-7 处理这个中断

# 把中断 28 绑定到 CPU 0(掩码 01 = CPU 0)
$ sudo echo 1 > /proc/irq/28/smp_affinity

# 把中断 28-31 分别绑定到 CPU 0-3
for i in 28 29 30 31; do
    cpu=$(($i - 28))
    mask=$(printf "%x" $((1 << $cpu)))
    echo $mask | sudo tee /proc/irq/$i/smp_affinity
done

中断掩码是十六进制位掩码。01 = CPU 0,02 = CPU 1,04 = CPU 2,08 = CPU 3。

用 set_irq_affinity 脚本

手算位掩码容易出错,可以用社区脚本简化:

#!/bin/bash
# set_irq_affinity.sh
# 用法: ./set_irq_affinity.sh eth0 "0-3"

IFACE=$1
CPUS=$2

# 获取网卡 IRQ 列表
IRQS=$(grep "$IFACE" /proc/interrupts | cut -d: -f1 | tr -d ' ')

# 将 CPU 范围转换为掩码
CPU_MASK=$(cset shield --cpu $CPUS --print 2>/dev/null | head -1)

i=0
for IRQ in $IRQS; do
    # 为每个队列分配一个 CPU
    CPU=$(echo $CPUS | awk -F'[-,]' "{print \$$((i % $(echo $CPUS | tr ',' '\n' | wc -l) + 1))}")
    printf "%x\n" $((1 << $CPU)) > /proc/irq/$IRQ/smp_affinity
    echo "IRQ $IRQ -> CPU $CPU"
    i=$((i + 1))
done

生产实战:完整配置方案

场景描述

一台双路 E5-2680 v4 服务器(共 56 核 112 线程,256GB 内存),跑三类工作负载:

  1. 低延迟交易引擎(需要独占 CPU,微秒级响应)
  2. MySQL 数据库(需要 NUMA 亲和,高吞吐)
  3. 系统服务和监控(常规优先级)

CPU 分配方案

NUMA Node 0 (CPU 0-27, 物理核 0-13, 超线程 28-41)
  ├── CPU 0-1: 系统服务 + 中断处理
  ├── CPU 2-13, 28-39: 交易引擎(隔离)
  └── CPU 40-41: 备用

NUMA Node 1 (CPU 14-27, 物理核 56-83, 超线程 84-111)
  ├── CPU 56-69, 84-97: MySQL(NUMA bind)
  └── CPU 70-71, 98-111: 系统服务和监控

完整配置脚本

#!/bin/bash
# cpu-isolation-setup.sh
# 生产环境 CPU 隔离与 NUMA 调优脚本
# 在 Ubuntu 22.04 (内核 5.15) 上测试通过

set -euo pipefail

echo "=== 1. 配置 GRUB 启动参数 ==="

# 检查是否已配置
if grep -q "isolcpus" /proc/cmdline; then
    echo "isolcpus 已在启动参数中,跳过 GRUB 配置"
else
    echo "请手动编辑 /etc/default/grub,添加以下参数:"
    echo 'GRUB_CMDLINE_LINUX="... isolcpus=2-13,28-39 nohz_full=2-13,28-39 rcu_nocbs=2-13,28-39"'
    echo "然后执行: sudo update-grub && sudo reboot"
    exit 1
fi

echo "=== 2. 关闭 irqbalance ==="
sudo systemctl stop irqbalance 2>/dev/null || true
sudo systemctl disable irqbalance 2>/dev/null || true
echo "irqbalance 已关闭"

echo "=== 3. 绑定网卡中断到 CPU 0-1 ==="
# 获取网卡中断号
NIC_IRQS=$(grep -E "eth0|ens192|enp" /proc/interrupts | awk -F: '{print $1}' | tr -d ' ')
i=0
for IRQ in $NIC_IRQS; do
    # 轮流分配到 CPU 0 和 1
    CPU=$((i % 2))
    MASK=$(printf "%x" $((1 << CPU)))
    echo $MASK > /proc/irq/$IRQ/smp_affinity 2>/dev/null || true
    echo "IRQ $IRQ -> CPU $CPU"
    i=$((i + 1))
done

echo "=== 4. 迁移隔离核上的内核线程 ==="
# 查找在隔离核上运行的内核线程
ISOLATED_CPUS="2-13,28-39"
for pid in $(pgrep -f "rcu\|kworker\|ksoftirqd\|migration\|watchdog"); do
    CURRENT_AFFINITY=$(taskset -pc $pid 2>/dev/null | grep -oP 'list: \K.*' || echo "")
    if [ -n "$CURRENT_AFFINITY" ]; then
        # 检查是否在隔离核上
        if taskset -pc $pid 2>/dev/null | grep -qP "[2-9]|1[0-3]|2[8-9]|3[0-9]"; then
            # 迁移到 CPU 0
            taskset -pc 0 $pid 2>/dev/null || true
            echo "Migrated kernel thread $pid to CPU 0"
        fi
    fi
done

echo "=== 5. 配置 MySQL NUMA 亲和性 ==="
# 使用 systemd 管理 MySQL 的 NUMA 配置
# 确认 MySQL 服务存在
if systemctl list-unit-files | grep -q mysql; then
    echo "建议在 MySQL systemd 服务中添加:"
    echo "  NUMAPolicy=bind"
    echo "  NUMAMask=1"
    echo "  CPUAffinity=56-69,84-97"
fi

echo "=== 6. 验证隔离效果 ==="
echo "--- 隔离核上的进程(应该只有手动绑定的)---"
for cpu in 2 3 4 5; do
    PROCS=$(ps -eLo pid,psr,comm | awk -v c=$cpu '$2==c {print $1, $3}')
    if [ -n "$PROCS" ]; then
        echo "CPU $cpu: $PROCS"
    fi
done

echo "--- NUMA 统计 ---"
numastat

echo "=== 配置完成 ==="

验证隔离效果

配置完成后,用以下方法验证:

# 1. 确认 isolcpus 生效
cat /proc/cmdline | grep isolcpus

# 2. 查看隔离核上是否有非预期进程
# 正常情况下,隔离核上应该空闲或只有你绑定的进程
mpstat -P 2-5 1 5
# 关注 %idle 列,隔离核如果没绑进程应该接近 100%

# 3. 检查 NUMA 内存分配
numastat -p <PID>
# 关注 Total 列,确认内存在正确节点上

# 4. 测量延迟改善效果
# 用 cyclictest 测量调度延迟
cyclictest -p 80 -t 1 -a 4 -d 0 -i 1000 -l 100000
# -a 4: 绑定到 CPU 4
# 比较隔离前后的最大延迟值

容器环境下的 CPU 隔离

Kubernetes 中的 CPU 隔离

在 Kubernetes 中,普通的 CPU request/limit 使用的是 CFS 带宽控制,不提供独占隔离。要实现真正的 CPU 独占,需要使用 static CPU Manager 策略。

# kubelet 配置(/var/lib/kubelet/config.yaml)
kind: KubeletConfiguration
cpuManagerPolicy: static
reservedSystemCPUs: "0,1"

然后 Pod 声明整数 CPU request:

apiVersion: v1
kind: Pod
metadata:
  name: critical-app
spec:
  containers:
  - name: app
    image: critical-app:latest
    resources:
      requests:
        cpu: "4"       # 必须是整数,才会触发独占分配
        memory: "8Gi"
      limits:
        cpu: "4"
        memory: "8Gi"
  # 指定 NUMA 亲和性(需要 Topology Manager)
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname

注意:CPU Manager 的 static 策略要求 CPU request 是整数。如果是 500m(0.5 核),走的是普通 CFS 限流,不会独占。

Docker 中的 CPU 隔离

# 使用 --cpuset-cpus 绑定容器到特定 CPU
docker run -d \
    --name critical-app \
    --cpuset-cpus="4-7" \
    --memory="16g" \
    --memory-swappiness=0 \
    critical-app:latest

Docker 的 --cpuset-cpus 只限制容器能用哪些 CPU,但不阻止其他容器也使用这些 CPU。要真正独占,需要配合宿主机的 isolcpus。

常见问题与排坑

问题一:isolcpus 配了但隔离核上还有进程

isolcpus 只阻止普通进程调度,内核线程不受限制。常见的"漏网之鱼":

# 查看隔离核上的内核线程
ps -eLo pid,psr,comm | awk '$2 >= 2 && $2 <= 13 {print}'
# 可能看到 kworker、ksoftirqd、migration、watchdog 等

# 手动迁移这些线程到非隔离核
for pid in $(pgrep kworker); do
    taskset -pc 0 $pid 2>/dev/null
done
for pid in $(pgrep ksoftirqd); do
    taskset -pc 0 $pid 2>/dev/null
done

但注意:某些内核线程(如 migration)是不能迁移的,强行迁移会报错。这些线程对性能影响很小,可以忽略。

问题二:numactl 绑定后程序 OOM

# 错误:只绑定了内存节点,但节点内存不够
$ numactl --membind=1 ./app
# 结果:Node 1 内存用完,程序 OOM 退出

# 正确:先检查节点可用内存
$ numactl --hardware | grep "node 1 free"
node 1 free: 89012 MB

# 如果内存不够,用 interleave 模式
$ numactl --interleave=all ./app

问题三:cgroup v2 中 cpuset 不生效

cgroup v2 默认不启用 cpuset 控制器,需要手动开启:

# 检查 cpuset 是否可用
cat /sys/fs/cgroup/cgroup.controllers
# 如果输出中没有 cpuset,需要在根 cgroup 启用

# 启用 cpuset 控制器
echo +cpuset > /sys/fs/cgroup/cgroup.subtree_control

问题四:隔离后性能反而下降

这种情况通常是过度隔离导致的。常见原因:

  1. 隔离了太多 CPU 核,系统服务被挤到少数核上,导致系统服务成为瓶颈
  2. 没有配合 NUMA 绑定,导致跨节点内存访问
  3. 网卡中断全绑到少数核上,导致中断处理跟不上

建议:先隔离 2-4 个核做测试,观察效果再逐步扩大。

问题五:容器中 –cpuset-cpus 和宿主机 isolcpus 冲突

# 宿主机配置了 isolcpus=4-7
# 容器配置了 --cpuset-cpus="4-7"

# 这种情况下,容器可以正常使用 CPU 4-7
# 因为 isolcpus 阻止的是"普通调度",taskset/cpuset 是"显式分配"
# 两者不冲突

但如果宿主机配置了 isolcpus=4-7,容器想用 CPU 0-3(非隔离核),也没问题。冲突场景是:容器想用 4-7 但宿主机另有 cpuset 把这些核分给了别的容器。这种情况以 cgroup v2 的 cpuset.cpus 为准。

性能基准测试

测试环境

  • 服务器:双路 Intel Xeon E5-2680 v4(共 28 核 56 线程)
  • 内存:256GB DDR4 ECC
  • 操作系统:Ubuntu 22.04 LTS(内核 5.15.0-91)
  • 测试工具:cyclictest(调度延迟)、sysbench(综合性能)、iperf3(网络性能)

调度延迟对比

# 未隔离
$ cyclictest -p 80 -t 1 -a 0 -i 1000 -l 100000
T:0 ( 1234) P:80 I:1000 C:100000 Min: 3 Act: 5 Avg: 8 Max: 47

# isolcpus 隔离 + nohz_full
$ cyclictest -p 80 -t 1 -a 4 -i 1000 -l 100000
T:0 ( 1234) P:80 I:1000 C:100000 Min: 2 Act: 3 Avg: 4 Max: 12
配置最小延迟平均延迟最大延迟改善幅度
未隔离3μs8μs47μs基准
isolcpus2μs4μs12μs最大延迟降低 74%
isolcpus + nohz_full + rcu_nocbs2μs3μs9μs最大延迟降低 81%

NUMA 亲和性对比(MySQL sysbench)

配置QPSP95 延迟CPU 使用率
默认(无 NUMA 配置)8,20012ms65%
interleave11,5008ms70%
bind(单节点)9,8005ms60%
bind + cpuset 隔离12,1004ms72%

bind + cpuset 隔离的组合效果最好——NUMA 绑定减少内存延迟,cpuset 隔离减少 CPU 争抢,两个优化叠加效果超过单独使用。

总结

CPU 隔离和 NUMA 调优是性能优化的"最后一公里"。加 CPU、加内存是横向扩展,CPU 隔离是纵向优化——同样的硬件,通过消除干扰和跨节点访问,榨出更多性能。

核心要点:

分层隔离。isolcpus 做启动级隔离(最强),cpuset 做运行时隔离(最灵活),taskset 做快速绑核(最简单)。三者配合使用,isolcpus 打底 + cpuset 管理服务 + taskset 临时调试。

NUMA 要和 CPU 绑定一起做。只绑 CPU 不绑内存等于白做一半——CPU 在 Node 0 上跑,数据在 Node 1 上存,每条内存访问都跨节点。用 numactl --cpunodebind=0 --membind=0 把两者绑定到同一节点。

中断必须管。隔离核上的硬件中断会打断你的实时线程。关掉 irqbalance,手动把网卡中断绑到非隔离核上。

容器环境特殊处理。Kubernetes 的 CPU Manager static 策略可以给容器独占 CPU,但要求整数 CPU request。Docker 的 –cpuset-cpus 需要配合宿主机 isolcpus 才能真正独占。

先测再上。隔离不是越多越好。过度隔离会导致系统服务被挤压,反而出问题。先用 cyclictest 和 sysbench 做基准测试,确认改善效果再扩大范围。

最后提一句:这套方案适用于物理机或独占型虚拟机。云服务器(AWS、阿里云等)的虚拟化层会干扰 NUMA 拓扑暴露,numactl --hardware 可能看不到真实的 NUMA 拓扑。云上环境的 CPU 隔离效果不如物理机理想,但这不是不做的理由——即使是虚拟化环境,taskset 和 cpuset 仍然能减少上下文切换,带来可观的延迟优化。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. 基于CPU隔离技术提升关键业务性能,最大化硬件利用率 — CSDN博主,isolcpus 与 cpuset 的配置方法与测试对比
  2. 别再乱配isolcpus了!深入Linux内核cmdline解析,避开CPU隔离的5个常见配置误区 — CSDN博主,isolcpus 内核解析与配置误区分析
  3. Linux cgroup v2 资源控制实战 — CSDN博主,cgroup v2 cpuset 配置方法
  4. Linux 组调度与容器编排:Kubernetes 的 CPU 资源分配底层 — CSDN博主,Kubernetes CPU 管理与 CFS 组调度原理
  5. 【Linux性能调优核心技巧】:CPU亲和性绑定的5种高阶用法 — CSDN博主,CPU亲和性绑定的多种实现方式
  6. Red Hat Enterprise Linux 性能调优指南 - CPU 调度 — Red Hat,CPU 调度策略与 NUMA 拓扑
  7. linux下CPU绑定、任务绑核、IRQ绑核具体怎么操作? — CSDN博主,taskset、sched_setaffinity 与 IRQ 绑核操作