概述
线上跑着一个高频交易系统,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 时间片,调度器在所有可用核心之间自由迁移进程。
听起来没问题,但对延迟敏感的场景是个灾难:
- 上下文切换开销:线程从 CPU A 迁移到 CPU B,L1/L2 缓存全部失效,需要重新从内存加载数据。一次迁移的代价是微秒级的延迟抖动。
- 缓存污染:其他进程跑在你的目标核上,把你之前缓存的数据挤出去,下次你的线程回来时全是 cache miss。
- 中断干扰:网卡中断、定时器中断随时打断你的线程。对于要求微秒级响应的系统,一次中断就是一次延迟尖峰。
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-7 | RCU 回调不在隔离核上执行 | 进一步减少内核干扰 |
isolcpus=4-7,domain | 将隔离核排除在负载均衡域外 | 默认已启用,显式声明 |
isolcpus=4-7,managed_irq | 限制中断亲和性 | 减少硬件中断干扰 |
三个参数配合使用,效果最佳。nohz_full 减少定时器中断,rcu_nocbs 把 RCU 回调移走,isolcpus 阻止普通进程调度——三层隔离叠加,隔离核上基本只剩下你手动绑定的进程。
isolcpus 的局限
isolcpus 不是万能的:
- 需要重启:修改后必须重启系统才能生效,不适合动态调整
- 不完全隔离:内核线程和某些中断仍然可能在隔离核上运行,需要手动迁移
- 与 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 ./app | CPU 密集型,延迟敏感 | 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):
| 配置 | QPS | P99 延迟 | 说明 |
|---|---|---|---|
| 默认(无 NUMA 配置) | 12,000 | 8ms | 内存跨节点访问严重 |
| interleave 模式 | 18,000 | 5ms | 内存均衡分布,带宽提升 |
| bind 模式(单节点) | 15,000 | 3ms | 延迟最优,但带宽受限 |
结论很明确:要带宽用 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 内存),跑三类工作负载:
- 低延迟交易引擎(需要独占 CPU,微秒级响应)
- MySQL 数据库(需要 NUMA 亲和,高吞吐)
- 系统服务和监控(常规优先级)
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
问题四:隔离后性能反而下降
这种情况通常是过度隔离导致的。常见原因:
- 隔离了太多 CPU 核,系统服务被挤到少数核上,导致系统服务成为瓶颈
- 没有配合 NUMA 绑定,导致跨节点内存访问
- 网卡中断全绑到少数核上,导致中断处理跟不上
建议:先隔离 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μs | 8μs | 47μs | 基准 |
| isolcpus | 2μs | 4μs | 12μs | 最大延迟降低 74% |
| isolcpus + nohz_full + rcu_nocbs | 2μs | 3μs | 9μs | 最大延迟降低 81% |
NUMA 亲和性对比(MySQL sysbench)
| 配置 | QPS | P95 延迟 | CPU 使用率 |
|---|---|---|---|
| 默认(无 NUMA 配置) | 8,200 | 12ms | 65% |
| interleave | 11,500 | 8ms | 70% |
| bind(单节点) | 9,800 | 5ms | 60% |
| bind + cpuset 隔离 | 12,100 | 4ms | 72% |
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 仍然能减少上下文切换,带来可观的延迟优化。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- 基于CPU隔离技术提升关键业务性能,最大化硬件利用率 — CSDN博主,isolcpus 与 cpuset 的配置方法与测试对比
- 别再乱配isolcpus了!深入Linux内核cmdline解析,避开CPU隔离的5个常见配置误区 — CSDN博主,isolcpus 内核解析与配置误区分析
- Linux cgroup v2 资源控制实战 — CSDN博主,cgroup v2 cpuset 配置方法
- Linux 组调度与容器编排:Kubernetes 的 CPU 资源分配底层 — CSDN博主,Kubernetes CPU 管理与 CFS 组调度原理
- 【Linux性能调优核心技巧】:CPU亲和性绑定的5种高阶用法 — CSDN博主,CPU亲和性绑定的多种实现方式
- Red Hat Enterprise Linux 性能调优指南 - CPU 调度 — Red Hat,CPU 调度策略与 NUMA 拓扑
- linux下CPU绑定、任务绑核、IRQ绑核具体怎么操作? — CSDN博主,taskset、sched_setaffinity 与 IRQ 绑核操作