1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Linux 性能调优系列(一) 建立科学的性能分析思路
  2. Linux 性能调优系列(二) 掌握基础性能监控命令
  3. Linux 性能调优系列(三) sysstat实现性能数据持久采集
  4. Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
  5. Linux 性能调优系列(五) 给你的服务器做一次全面体检
  6. Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
  7. Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
  8. Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
  9. Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
  10. Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
  11. Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
  12. Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
  13. Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
  14. Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
  15. Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
  16. Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
  17. Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
  18. Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
  19. Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
  20. Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
  21. Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
  22. Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
  23. Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
  24. Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
  25. Linux 性能调优系列(二十五) 聊聊VirtIO、TuneD、virtio-mem 调优

上一篇我们讲了实时调度策略。但你可能已经发现,把任务设成 SCHED_FIFO 优先级 50,延迟抖动依然存在。这一篇我们来讲:优先级之外,还有哪些东西在干扰你的任务,怎么把它们赶走。

先看一个典型现象,我在培训里经常遇到这样的提问:

老师,机器 CPU 才 20%,业务线程也设了实时优先级,为什么 P99 延迟还是时不时飙一下?

这个问题的答案,往往不在调度策略里,而在”这个 CPU 上到底还有谁”。

为什么要关心 CPU 在哪里跑

线程迁移让缓存变冷

线程从 CPU 2 迁到 CPU 5,它在 CPU 2 的 L1/L2 里积累的热数据就用不上了。数据并没有”失效”,而是在新 CPU 上变冷了,要重新从内存或 L3 加载。这就是延迟抖动的一个来源。

中断会直接打断你

网卡收包、磁盘完成 I/O,都会产生中断。中断不看你的线程优先级,来了就打断。 你的实时线程再高优先级,也得让路。

同一个 CPU 上还住着别人

后台服务、监控 agent、kworker、ksoftirqd 都可能在你的 CPU 上排队。

两个概念要分清:

概念回答的问题性质
CPU 亲和性这个任务只能在哪些 CPU 上跑?限制任务自己
CPU 隔离别的东西能不能不要来这些 CPU?限制别人

很多人只做了前者就以为万事大吉,这是最常见的误区,后面会专门讲。

flowchart TB
    G["目标:关键线程在 CPU 2-3 上不被打扰"] --> A["业务线程绑核<br/>taskset / CPUAffinity / cpuset"]
    G --> B["其他用户态进程让开<br/>系统服务只用 CPU 0-1"]
    G --> C["中断让开<br/>smp_affinity / irqbalance / managed_irq"]
    G --> D["内核干扰降低<br/>nohz_full / rcu_nocbs"]
    G --> E["实测验证<br/>/proc/interrupts / rtla / cyclictest"]

这五件事缺一不可,后面的内容就是按这张图逐个展开。

先学会看 CPU 拓扑

CPU 列表与位掩码

Linux 里表示 CPU 集合有两种写法,很容易混淆。

列表写法,人类友好:

1
2
3
0-3        # CPU 0,1,2,3
0,2,4 # CPU 0,2,4
0-1,3-5 # CPU 0,1,3,4,5

十六进制位掩码,每一位对应一个 CPU,最低位是 CPU 0:

1
2
3
f   = 1111 → CPU 0-3
c = 1100 → CPU 2,3
4 = 0100 → CPU 2

这很重要,因为不同接口吃不同的格式:

接口格式
taskset -p 的输出掩码
taskset -cp 的输出列表
/proc/irq/N/smp_affinity十六进制掩码
/proc/irq/N/smp_affinity_list列表

写错格式的后果很有意思:往 smp_affinity 里 echo 2,你以为是 CPU 2,实际是掩码 0x2 = CPU 1。新手建议一律用 _list 结尾的接口,不容易算错。

例如:

1
2
3
4
[root@localhost ~]# taskset -p 1013
pid 1013's current affinity mask: f
[root@localhost ~]# taskset -cp 1013
pid 1013's current affinity list: 0-3

lscpu 看什么

1
lscpu
字段含义
CPU(s)逻辑 CPU 总数
Thread(s) per core每个物理核有几个逻辑核(超线程,大于 1 说明开了 SMT)
Core(s) per socket每个插槽有几个物理核
Socket(s)插槽数
NUMA node(s)NUMA 节点数
On-line CPU(s) list当前在线的 CPU

看老李的样例:

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
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
64
65
66
[root@localhost ~]# lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 45 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Vendor ID: GenuineIntel
BIOS Vendor ID: GenuineIntel
Model name: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BIOS Model name: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz CPU @ 2.3GHz
BIOS CPU family: 2
CPU family: 6
Model: 141
Thread(s) per core: 1
Core(s) per socket: 2
Socket(s): 2
Stepping: 1
BogoMIPS: 4608.00
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat p
se36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm c
onstant_tsc arch_perfmon rep_good nopl xtopology tsc_reliable nonst
op_tsc cpuid tsc_known_freq pni pclmulqdq vmx ssse3 fma cx16 pcid s
se4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f
16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stib
p ibrs_enhanced tpr_shadow ept vpid ept_ad fsgsbase tsc_adjust bmi1
avx2 smep bmi2 erms invpcid avx512f avx512dq rdseed adx smap avx51
2ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xs
avec xgetbv1 xsaves user_shstk arat vnmi avx512vbmi umip pku ospke
avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_
vpopcntdq rdpid movdiri movdir64b fsrm avx512_vp2intersect md_clear
flush_l1d arch_capabilities
Virtualization features:
Virtualization: VT-x
Hypervisor vendor: VMware
Virtualization type: full
Caches (sum of all):
L1d: 192 KiB (4 instances)
L1i: 128 KiB (4 instances)
L2: 5 MiB (4 instances)
L3: 48 MiB (2 instances)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-3
Vulnerabilities:
Gather data sampling: Unknown: Dependent on hypervisor status
Indirect target selection: Mitigation; Aligned branch/return thunks
Itlb multihit: KVM: Mitigation: Split huge pages
L1tf: Not affected
Mds: Not affected
Meltdown: Not affected
Mmio stale data: Not affected
Old microcode: Not affected
Reg file data sampling: Not affected
Retbleed: Not affected
Spec rstack overflow: Not affected
Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl
Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitizatio
n
Spectre v2: Mitigation; Enhanced / Automatic IBRS; IBPB conditional; PBRSB-eIBR
S SW sequence; BHI SW loop, KVM SW loop
Srbds: Not affected
Tsa: Not affected
Tsx async abort: Not affected
Vmscape: Not affected
[root@localhost ~]#

超线程兄弟核

如果开着超线程,两个逻辑核共享同一个物理核的执行单元。你隔离了 CPU 2,但它的兄弟核 CPU 6 上跑着别的负载,照样会干扰你。

1
2
3
4
5
6
7
8
[root@localhost ~]# cat /sys/devices/system/cpu/cpu2/topology/thread_siblings_list
2
[root@localhost ~]# lscpu -e # 看每个逻辑 CPU 对应的 CORE 和 NODE
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
0 0 0 0 0:0:0:0 yes
1 0 0 1 1:1:1:0 yes
2 0 1 2 2:2:2:1 yes
3 0 1 3 3:3:3:1 yes

生产建议:隔离的时候,要么把兄弟核成对隔离,要么干脆关闭 SMT。

NUMA:CPU 和内存是”邻居”关系

多路服务器上,每个插槽有自己的核心、缓存和本地内存。访问本地内存快,访问别的节点的内存慢。

现代 CPU 一个插槽内也可能划成多个 NUMA 节点(比如 AMD 的 NPS 模式、Intel 的 SNC),所以别想当然地认为”一个插槽 = 一个节点”,以 lscpu 为准。

1
2
3
4
5
6
7
8
9
[root@localhost ~]# dnf install numactl -y
[root@localhost ~]# numactl -H
available: 1 nodes (0)
node 0 cpus: 0 1 2 3
node 0 size: 7903 MB
node 0 free: 7248 MB
node distances:
node 0
0: 10

重点看每个节点有哪些 CPU、多少内存,以及节点之间的 distance。

这对绑核的意义是:绑核不能只看 CPU 编号。 你把线程绑到节点 1 的 CPU,但内存是在节点 0 上分配的,每次访问都要跨节点,延迟反而更差。

让 CPU 和内存都限制在节点 0:

1
2
[root@localhost ~]# numactl --cpunodebind=0 --membind=0 sleep 100000 &
[1] 4879

看每个节点的内存分配情况:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# numastat -p $(pidof sleep)

Per-node process memory usage (in MBs)
PID Node 0 Total
---------------- --------------- ---------------
4879 (sleep) 1.87 1.87 #看这一行
5265 (sleep) 1.97 1.97
---------------- --------------- ---------------
Total 3.84 3.84

--cpunodebind=0:只允许进程运行在 NUMA 节点 0 的 CPU 上。这是 CPU 亲和性,效果类似 taskset,但是按”节点”而不是按 CPU 编号来指定。

--membind=0:只允许进程从节点 0 的内存里分配。这是严格限制:如果节点 0 的内存用完了,不会去别的节点借,而是直接分配失败,严重时触发 OOM。

两个合起来的意思是:线程只在节点 0 的 CPU 上跑,内存也只从节点 0 分配,所以所有内存访问都是本地访问,没有跨节点的额外延迟。

flowchart LR
    subgraph N0["NUMA 节点 0"]
        C0["CPU 0-3"] --- M0["本地内存"]
    end
    subgraph N1["NUMA 节点 1"]
        C1["CPU 4-7"] --- M1["本地内存"]
    end
    P["进程<br/>cpunodebind=0<br/>membind=0"] --> C0
    P -.只用这里的内存.-> M0
    C0 -. 跨节点访问,慢 .-> M1

一个常见的坑:membind 太严格,--membind 是硬限制,节点内存不够就失败。如果你只是希望”优先用本地内存,不够再借别的节点”,改用:

1
numactl --cpunodebind=0 --preferred=0 sleep 10000 &
参数行为
--membind=0只能用节点 0 的内存,用完就失败
--preferred=0优先用节点 0,不够再用别的节点
--interleave=all内存在所有节点间轮流分配,适合需要大内存带宽、不在乎单次延迟的场景(比如数据库)
--localalloc在进程当前运行的 CPU 所在节点上分配内存

另一个相关写法,如果你想按具体 CPU 编号而不是节点来指定,用 --physcpubind(或 -C):

1
numactl --physcpubind=2,3 --membind=0 sleep 10000 &

这样 CPU 精确到 2、3,内存仍限定在节点 0。

还有一个常被忽略的点:网卡也挂在某个 NUMA 节点上。,处理这块网卡数据的业务线程,最好和网卡在同一个节点上。-1 的意思是:内核没有这块设备的 NUMA 节点归属信息,也就是”不属于任何特定节点 / 未知”。

1
2
3
4
5
[root@localhost ~]# ip -br a
lo UNKNOWN 127.0.0.1/8 ::1/128
ens160 UP 192.168.8.128/24 fe80::20c:29ff:fe29:c131/64
[root@localhost ~]# cat /sys/class/net/ens160/device/numa_node
-1

从网卡名 ens160 和 MAC 前缀 00:0c:29(VMware)来看,机器是 VMware 虚拟机,ens160 是虚拟网卡 vmxnet3,直接是-1,因为

  1. 虚拟机本身没有 NUMA 拓扑:只有一个 node 0,虚拟 PCI 设备也就没有”挂在哪个节点”的概念;
  2. 虚拟化层没有把这个信息传给客户机:即使宿主机是多路服务器,虚拟网卡也不是真实的物理 PCIe 设备,没有物理插槽归属;
  3. 物理机上偶尔也会出现:BIOS/ACPI 没有提供该设备的节点信息(比如 ACPI 表里缺少 _PXM,或者开启了节点交错 / NUMA 被关闭)。

taskset:快速验证的好工具

taskset 最适合”先试一下效果”。

查看,输出掩码

1
2
3
[root@localhost ~]# taskset -p 1013
pid 1013's current affinity mask: f

查看,输出列表(更直观)

1
2
3
[root@localhost ~]# taskset -cp 1013
pid 1013's current affinity list: 0-3

修改已运行的进程,注意要带 -p:

1
2
3
4
[root@localhost ~]# taskset -cp 2 1013
pid 1013's current affinity list: 0-3
pid 1013's new affinity list: 2

启动新进程时不带 -p:

1
2
3
4
5
6
[root@localhost ~]# taskset -c 2 sleep 10000 &
[2] 9398
[root@localhost ~]# taskset -p 9398
pid 9398's current affinity mask: 4
[root@localhost ~]# taskset -cp 9398
pid 9398's current affinity list: 2

这里和上一篇的 chrt 一样:改已有进程用 -p,启动新进程不用。 如果把 taskset -c 2 1013 当成改 PID,它会去执行一个叫 1013 的命令然后报错。

多线程程序的坑

taskset -cp 2 1013 只修改了线程 1013 自己,也就是主线程。进程里已经存在的其他线程不受影响。加 -a,修改进程内所有已存在的线程

1
taskset -acp 2 1013

另外,新创建的线程会继承创建者的亲和性,子进程也一样。

亲和性是硬限制

taskset 设置的是硬限制:线程只能在这些 CPU 上跑,即使别的 CPU 全空闲也不行。所以别绑得太死。如果你只绑一个 CPU,那个 CPU 上一旦有人占着,你的线程就只能等。taskset 的修改是临时的,进程重启就没了,只适合验证。

systemd:让绑核”留下来”

生产环境里的服务应该交给 systemd 管理。

1
2
[root@localhost ~]# mkdir /etc/systemd/system/sshd.service.d/
[root@localhost ~]# vi /etc/systemd/system/sshd.service.d/override.conf

在打开的编辑器里写:

1
2
[Service]
CPUAffinity=0-1

然后:

1
2
[root@localhost ~]# systemctl daemon-reload
[root@localhost ~]# systemctl restart sshd

验证

1
2
3
4
[root@localhost ~]# systemctl show sshd -p CPUAffinity
CPUAffinity=0-1
[root@localhost ~]# taskset -acp $(systemctl show sshd -p MainPID --value)
pid 11861's current affinity list: 0,1

这个例子其实很有代表性:**把 sshd 这类”非关键”服务绑到 CPU 0-1,就是在给隔离核让路。**还可以做全局设置。编辑 /etc/systemd/system.conf:

1
2
[Manager]
CPUAffinity=0-1

这样 systemd 启动的所有服务默认只用 CPU 0-1,需要关键 CPU 的服务再单独覆盖。改完建议重启,对新启动的服务才完整生效。

注意:CPUAffinity 只约束服务自己。它不会把别人赶出你的 CPU,也不处理中断。

cgroup v2 cpuset:按组划分 CPU

如果要给”一组任务”划地盘,用 cpuset。先确认是 cgroup v2:

1
2
[root@localhost ~]# stat -fc %T /sys/fs/cgroup
cgroup2fs

手工体验一遍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 第一步:让根 cgroup 把 cpuset 控制器交给子 cgroup
echo +cpuset > /sys/fs/cgroup/cgroup.subtree_control

# 第二步:创建子 cgroup
mkdir /sys/fs/cgroup/rt_group

# 第三步:设置可用 CPU
echo 2-3 > /sys/fs/cgroup/rt_group/cpuset.cpus

# 第四步:把进程放进去
echo 13400 > /sys/fs/cgroup/rt_group/cgroup.procs

# 验证
[root@localhost ~]# grep Cpus_allowed_list /proc/13400/status
Cpus_allowed_list: 2-3

第一步最容易漏。 不启用控制器,rt_group 目录里根本不会有 cpuset.cpus 文件。

关于内存节点,v2 里 cpuset.mems 默认继承父级,不是必须设置(v1 才是)。需要限制内存节点时再设:

1
echo 0 > /sys/fs/cgroup/rt_group/cpuset.mems

独占:用 partition,不是 cpu_exclusive

cpuset.cpu_exclusive 是 v1 的接口。v2 里要用 cpuset.cpus.partition:

1
2
3
4
[root@localhost ~]# echo isolated > /sys/fs/cgroup/rt_group/cpuset.cpus.partition
[root@localhost ~]# cat /sys/fs/cgroup/rt_group/cpuset.cpus.partition
isolated

它有三种取值:

值含义
member普通成员,默认值
root分区根,这些 CPU 独占给本组,组内仍然做负载均衡
isolated独占,并且关闭组内负载均衡,和 isolcpus 的效果类似,但可以动态调整

读回来如果看到 isolated invalid (...),括号里是失败原因,说明条件不满足,比如 CPU 被兄弟组占用了。

isolated 的一个后果:组内不再自动做负载均衡,你的线程不会自己跑到另一个 CPU 上去。所以组内线程要自己明确绑核,否则可能全挤在一个 CPU 上。

在 systemd 系统上更推荐

RHEL 上 cgroup 树是 systemd 管理的,直接手工 mkdir 容易和 systemd 打架。更稳妥的方式是让 systemd 来设:

1
2
3
[Service]
AllowedCPUs=2-3
AllowedMemoryNodes=0

或者临时调整:

1
systemctl set-property --runtime my_app.service AllowedCPUs=2-3

中断亲和性:让 IRQ 绕开你

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 看中断分布,每一列是一个 CPU 的累计次数
cat /proc/interrupts

# 看某个中断(比如 12)当前允许去哪些 CPU
[root@localhost ~]# cat /proc/irq/12/smp_affinity_list
0-3

# 实际生效的位置
[root@localhost ~]# cat /proc/irq/12/effective_affinity_list
0


# 修改
echo 0-1 > /proc/irq/12/smp_affinity_list

这里把中断绑到 CPU 0-1,也就是”系统核”,给 CPU 2-3 清净。还有一个容易忽略的参数:

1
2
[root@localhost ~]# cat /proc/irq/default_smp_affinity
ffffffff,ffffffff,ffffffff,ffffffff

它决定新注册的中断默认去哪,修改已有中断不会影响它。

为什么有时候改了没用

  1. irqbalance 在后台”纠正”你。 你手动改完,它下个周期又给你调回去了;
  2. managed IRQ。 现代多队列网卡和 NVMe,内核按 CPU 拓扑自动管理它们的中断,用户态改不了,写入通常会报 I/O 错误;
  3. 部分中断有设备或平台约束。

对于 managed IRQ,要靠内核参数 isolcpus=managed_irq,... 让内核自己避开隔离核,这点下面讲 tuned 时会再提到。

irqbalance:自动平衡的”双刃剑”

irqbalance 的目标是把中断均匀撒在各个 CPU 上,这对通用服务器很好,但和”隔离”的目标正好相反。

1
2
3
4
5
6
7
8
9
[root@localhost ~]# systemctl status irqbalance
○ irqbalance.service - irqbalance daemon
Loaded: loaded (/usr/lib/systemd/system/irqbalance.service; enabled; preset: enabled)
Active: inactive (dead)
Condition: start condition unmet at Sun 2026-10-04 12:40:16 CST; 2min 31s ago
└─ ConditionCPUs=>1 was not met
Docs: man:irqbalance(1)
https://github.com/Irqbalance/irqbalance

处理方式有两种。

方式一:直接停掉,完全手工管理

1
systemctl disable --now irqbalance

方式二:留着,但告诉它哪些 CPU 别碰

编辑 /etc/sysconfig/irqbalance:

1
IRQBALANCE_BANNED_CPULIST=2-3
1
systemctl restart irqbalance

这里禁的是 CPU:irqbalance 不会再往 CPU 2-3 上放中断。如果你想禁的是某个中断,用另一个参数:

1
IRQBALANCE_ARGS="--banirq=12"

这两个别混。很多文章把”禁 CPU”说成”禁中断”,导致配了半天不生效。

另外,如果你用了下面要讲的 tuned cpu-partitioning,它会自己处理 irqbalance 的禁用 CPU 列表,不要再手工去改,也不用手动停掉,两边都改容易互相覆盖。

tuned cpu-partitioning:生产环境的推荐姿势

前面这些手工操作,零散、容易漏、重启后要重做。生产环境推荐用 tuned 的 cpu-partitioning profile 一次性做到位。

它做了什么

flowchart TB
    T["tuned-adm profile cpu-partitioning"] --> K["内核启动参数<br/>isolcpus / nohz_full / rcu_nocbs"]
    T --> S["systemd 层面<br/>系统服务默认只用 housekeeping CPU"]
    T --> I["中断<br/>调整 irqbalance 禁用列表,迁移可迁移的 IRQ"]
    T --> Y["一批 sysctl 调整<br/>减少干扰"]
    K --> R["重启后生效"]

操作步骤

先装包,注意 profile 在单独的子包里,具体以 tuned-adm list 为准:

1
2
[root@localhost ~]# dnf install tuned tuned-profiles-cpu-partitioning -y

先写变量,再启用 profile,编辑 /etc/tuned/cpu-partitioning-variables.conf:

1
isolated_cores=2-3
1
2
3
[root@localhost ~]# systemctl enable tuned --now
[root@localhost ~]# tuned-adm profile cpu-partitioning
[root@localhost ~]# reboot

验证

1
2
3
[root@localhost ~]# tuned-adm active
[root@localhost ~]# tuned-adm verify
[root@localhost ~]# cat /proc/cmdline

cmdline 里应该能看到类似下面的内容,具体 flag 随 tuned 版本略有差异:

1
isolcpus=managed_irq,domain,2-3 nohz_full=2-3 rcu_nocbs=2-3

这几个内核参数到底干什么

参数含义
isolcpus=domain把这些 CPU 从调度域中拿掉,调度器不再往上做负载均衡,普通任务不会被自动迁过来
isolcpus=managed_irq让内核尽量不把 managed IRQ 放到这些 CPU 上
nohz_full当 CPU 上只有一个可运行任务时,停掉周期性调度 tick(忙碌时的 tick 抑制,不是空闲时)
rcu_nocbs把 RCU 回调处理从这些 CPU 卸载到别的 CPU 的内核线程上

nohz_full 有两个前提,不然不会生效:

  • 该 CPU 上同时只能有一个可运行任务;
  • 系统里必须留有 housekeeping CPU 来承接被卸载的工作(所以 CPU 0 通常不要隔离)。

一个很重要的认知:它不会帮你把业务绑过去

cpu-partitioning 只是清出了隔离核,不会把你的业务放进去。普通进程默认会被限制在 housekeeping CPU 上,你的关键业务必须显式绑核:

1
2
taskset -c 2-3 ./my_app
# 或者 systemd 里:CPUAffinity=2-3

tuna:交互式调整

tuna 适合现场排查和临时调整,可以同时看线程、中断,并移动它们。

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
[root@localhost ~]# dnf install tuna -y

[root@localhost ~]# tuna show_threads
thread ctxt_switches
pid SCHED_ rtpri affinity voluntary nonvoluntary cmd
1 OTHER 0 0,1 3625 5705 systemd
2 OTHER 0 0,1 375 3 kthreadd
3 OTHER 0 0,1 51 2 pool_workqueue_release
4 OTHER 0 0,1,2,3 2 0 kworker/R-rcu_gp
5 OTHER 0 0,1,2,3 2 0 kworker/R-sync_wq
6 OTHER 0 0,1 2 0 kworker/R-kvfree_rcu_reclaim
7 OTHER 0 0,1,2,3 2 0 kworker/R-slub_flushwq
8 OTHER 0 0,1 2 0 kworker/R-netns
9 OTHER 0 0 347 22 kworker/0:0-xfs-conv/dm-0
10 OTHER 0 0 2794 48 kworker/0:1-xfs-conv/dm-0
11 OTHER 0 0 4 0 kworker/0:0H-events_highpri
14 OTHER 0 0,1,2,3 2 0 kworker/R-mm_percpu_wq
15 OTHER 0 0 667 14 ksoftirqd/0

[root@localhost ~]# tuna show_irqs
# users affinity
0 timer 0,1,2,3
1 i8042 0,1
8 rtc0 0,1
9 acpi 0
12 i8042 0,1
14 ata_piix 0,1
15 ata_piix 0,1
16 vmwgfx 0

注意写法:先用 --threads / --irqs 选对象,再用 --move 执行动作。

1
2
3
4
5
6
7
# 把线程 1026 移到 CPU 2
[root@localhost ~]# taskset -acp $(systemctl show sshd -p MainPID --value)
pid 1026's current affinity list: 0,1
[root@localhost ~]# tuna move --cpus 2 --threads 1026

# 把中断 12 移到 CPU 2
tuna move --cpus=2 --irqs=12

还有一个很实用的动作,一键”清场”:

1
[root@localhost ~]# tuna isolate --cpus=2-3

它会把线程和中断尽量从 CPU 2-3 上挪走,挪不走的(比如每个 CPU 都有的内核线程)会保留。和 taskset 一样,tuna 的修改重启就丢,只用于测试,持久化交给 systemd 和 tuned。

内核线程到底赶不赶得走

这里要诚实地讲清楚一个很多资料不愿意说的事实:

隔离不等于清零。

内核里有些线程是每个 CPU 一个的,比如 migration/2、ksoftirqd/2、kworker/2:1。它们本来就是为这个 CPU 服务的,没法迁走。

所以在隔离核上执行 ps 看到这些线程是正常的。我们能做的是让它们少被触发:

  • 中断移走 → ksoftirqd 就没活干;
  • 业务不频繁触发内核工作 → kworker 就闲着;
  • nohz_full + rcu_nocbs → 减少 tick 和 RCU 的打扰。

低延迟场景的推荐流程

flowchart TD
    A["延迟抖动"] --> B{"确认是 CPU 竞争或中断造成的?<br/>先测量,不要盲目隔离"}
    B -- 是 --> C["规划:系统核 vs 业务核<br/>注意 SMT 兄弟核和 NUMA"]
    C --> D["tuned cpu-partitioning<br/>写变量 → 启用 → 重启"]
    D --> E["业务显式绑到隔离核"]
    E --> F["检查中断是否已避开"]
    F --> G["实测:cyclictest / rtla"]
    G --> H{"达标?"}
    H -- 否 --> I["查被忽略的干扰源<br/>回到第二步分析"]
    H -- 是 --> J["写进文档和配置管理"]

规划示例

1
2
CPU 0-1   系统核(housekeeping):系统服务、中断、监控
CPU 2-3 业务核:关键线程,注意是否是 SMT 兄弟

验证:不要只看配置,要看实际行为

看隔离核上有什么在跑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# psr 列是线程当前所在 CPU
[root@localhost ~]# ps -eLo psr,pid,tid,comm | awk '$1==2 || $1==3'
2 31 31 cpuhp/2
2 32 32 idle_inject/2
2 33 33 migration/2
2 34 34 ksoftirqd/2
2 35 35 kworker/2:0-slub_flushwq
2 36 36 kworker/2:0H-kblockd
3 38 38 cpuhp/3
3 39 39 idle_inject/3
3 40 40 migration/3
3 41 41 ksoftirqd/3
3 42 42 kworker/3:0-events
3 43 43 kworker/3:0H-events_highpri
2 65 65 kworker/2:1-events
3 72 72 kworker/3:1H
3 74 74 kworker/3:1-events
3 131 131 kworker/3:2
2 679 679 kworker/2:1H

除了你的业务线程和每 CPU 的内核线程,不该有别的。看中断有没有还往隔离核上打:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@localhost ~]# watch -d -n1 cat /proc/interrupts

CPU0 CPU1 CPU2 CPU3
0: 34 0 0 0 IR-IO-APIC 2-edge timer
1: 0 9 0 0 IR-IO-APIC 1-edge i8042
8: 0 0 0 0 IR-IO-APIC 8-edge rtc0
9: 0 0 0 0 IR-IO-APIC 9-fasteoi acpi
12: 15 0 0 0 IR-IO-APIC 12-edge i8042
14: 0 0 0 0 IR-IO-APIC 14-edge ata_piix
15: 0 0 0 0 IR-IO-APIC 15-edge ata_piix
16: 0 0 1334 0 IR-IO-APIC 16-fasteoi ehci_hcd:usb1, vmwgfx
18: 0 0 0 64 IR-IO-APIC 18-fasteoi uhci_hcd:usb2
19: 0 0 0 0 IR-IO-APIC 19-fasteoi snd_ens1371
24: 0 0 0 0 IR-PCI-MSI-0000:00:15.0 0-edge PCIe PME, pciehp
25: 0 0 0 0 IR-PCI-MSI-0000:00:15.1 0-edge PCIe PME, pciehp
26: 0 0 0 0 IR-PCI-MSI-0000:00:15.2 0-edge PCIe PME, pciehp

盯着 CPU 2、CPU 3 那两列,数字不该持续增长。LOC(本地定时器)一行在 nohz_full 生效后增长会非常慢,大约每秒一次,这是正常现象。

用工具量化干扰:

1
[root@localhost ~]# dnf install realtime-tests rtla -y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 测调度延迟
[root@localhost ~]# cyclictest -m -p 95 -a 2-3 -t -D 10m -q
# /dev/cpu_dma_latency set to 0us
^CT: 0 ( 6042) P:95 I:1000 C: 22116 Min: 3 Act: 291 Avg: 246 Max: 9639
T: 1 ( 6043) P:95 I:1500 C: 14748 Min: 4 Act: 226 Avg: 306 Max: 9691

# 测"噪声",也就是 CPU 被别的东西偷走了多少时间(rtla 包,以你的环境为准)
[root@localhost ~]# rtla osnoise top -c 2-3

Operating System Noise
duration: 0 00:00:12 | time is in us
CPU Period Runtime Noise % CPU Aval Max Noise Max Single HW NMI IRQ Softirq Thread
2 #11 11000000 125037 98.86330 19823 215 3197 0 6752 251 0
3 #11 11000000 123761 98.87490 17730 94 3036 0 6721 295 0

常见误区

误区一:绑了核就等于隔离

不是。taskset、CPUAffinity 只限制了自己。其他进程、中断、内核线程照样可以来。

误区二:隔离越多越好

隔离核上的东西都不能跑,系统管理、监控、中断处理全挤在剩下的 CPU 上,反而会拖垮整体。建议:housekeeping 核至少保留 1-2 个物理核,并且给它们留够处理中断的余量。

误区三:只改 isolcpus 就够了

isolcpus 只管调度器不往上放普通任务。完整隔离还要配合 nohz_full、rcu_nocbs、中断迁移,这也是推荐用 tuned 的原因。

误区四:隔离了就不管 SMT 兄弟核

隔离 CPU 2,它的兄弟核 CPU 6 上跑着别的,物理核的执行资源仍然被共享。要么成对隔离,要么关掉 SMT。

误区五:tuna / taskset 改了会保留

它们是临时的。要持久化就用 systemd、tuned。

误区六:改了配置就不用验证

隔离的效果必须靠 /proc/interrupts、ps、cyclictest、rtla 来验证。没有量化数据,就不要说”优化好了”。

命令速查

目的命令
看 CPU 拓扑lscpu -e
看 SMT 兄弟核cat /sys/devices/system/cpu/cpuN/topology/thread_siblings_list
看 NUMAnumactl -H
看网卡所在 NUMA 节点cat /sys/class/net/eth0/device/numa_node
查看进程亲和性taskset -cp PID
修改已有进程(所有线程)taskset -acp 2 PID
启动并绑核taskset -c 2 ./cmd
看实际允许的 CPUgrep Cpus_allowed_list /proc/PID/status
看中断分布cat /proc/interrupts
设置中断亲和echo 0-1 > /proc/irq/N/smp_affinity_list
启用 tuned 分区tuned-adm profile cpu-partitioning
验证 tunedtuned-adm verify
动态移动线程tuna move --cpus=2 --threads=PID
动态移动中断tuna move --cpus=2 --irqs=N
测延迟cyclictest -m -p 95 -a 2-3 -t -D 10m -q

课后思考

  1. 为什么 taskset -cp 2 PID 对一个 16 线程的程序可能”没效果”?应该怎么改?
  2. 一台 8 核 16 线程(开着 SMT)的机器,你想隔离 CPU 4-7。thread_siblings_list 显示 4 和 12 是一对,那应该隔离哪些 CPU 才合理?
  3. isolcpus 和 cpuset.cpus.partition=isolated 都能关闭负载均衡,它们的主要区别是什么?什么场景下该用哪个?
  4. 隔离核上 ps 能看到 ksoftirqd/3,这是不是说明隔离失败了?

参考手册

1
2
3
4
5
6
7
man taskset
man lscpu
man numactl
man tuned-adm
man tuna
man systemd.exec
man irqbalance