文章总结: 本文详细介绍了高频交易场景下CPU隔离的完整方案,指出仅绑核无法消除干扰,需组合使用isolcpus、nohzfull、rcunocbs和IRQ亲和性设置来分别关闭其他任务、时钟中断、RCU回调和硬件中断四类干扰源。文章提供了GRUB配置方法、验证手段(如检查/proc/interrupts的LOC行)以及量化剩余噪声的工具rtla,并强调了至少保留一个非隔离核等关键约束。 综合评分: 100 文章分类: 实战经验,解决方案,其他
高频交易低延迟优化:CPU隔离 isolcpus/nohz_full/RCU/IRQ
内存泄漏
2026年7月18日 21:24 辽宁
在小说阅读器读本章
去阅读
以下文章来源于Rust高频量化交易 ,作者lijiachang8
Rust高频量化交易 .
Rust高频量化交易系统
“
亲和性绑定只解决了”策略线程不乱跑”,没解决”别人不许上这个核”。本文讲清楚绑核之后还剩下哪几类干扰源,
isolcpus、nohz_full、rcu_nocbs和 IRQ 亲和性分别关掉了其中的哪一类,怎么配、怎么验证,以及每个开关背后的约束和坑。
一、绑核之后还剩什么
上一篇结尾留了个问题:策略线程绑到 CPU 3 之后,这个核并不是它独占的。具体来说,还有四类东西会跑到这个核上。
绑核之后,核心上仍有四类干扰
一是其它可运行任务。 亲和性掩码是单向的——它限制了你的线程只能在 CPU 3 上跑,但没有告诉调度器”别的任务不许上 CPU 3″。系统里任何没设亲和性的进程、内核线程、kworker,负载均衡时都可能被放到这个核上,跟策略线程抢时间片。
二是时钟中断(tick)。 内核按 CONFIG_HZ 的频率给每个核发本地时钟中断,典型值 250 或 1000,也就是每 4ms 或每 1ms 一次。tick 处理程序要做调度器记账、检查是否需要抢占、推进定时器轮、驱动 RCU 状态机。这些工作本身不长,但它是周期性的、无条件的,落在热路径上就是一次上下文切换加一串被冲掉的缓存行。
三是 RCU 回调。 RCU 的宽限期结束后,回调(大量是内存释放)默认在本地核的软中断上下文里执行。批量回调一次可能跑几十上百微秒,什么时候来完全取决于系统别处的活动。
四是硬件中断。 网卡收包、NVMe 完成队列都会触发 IRQ。默认情况下 irqbalance 会把中断在全核之间分配,你的”专属”核照样会被网卡中断打断。
这四类各有各的开关,缺一个都不算隔离干净。
这类干扰可以分成两半看:异步噪声是别人强加给你的——中断、定时器、调度器抢占,上面四条都属于这一类,靠配置关掉;同步噪声是你自己招来的——系统调用、缺页异常,只能靠改程序消除。本文讲的是前一半,但后一半同样不能漏,第六节会回到这个问题。
被赶走的活总要有地方去。承接这些工作的非隔离核(housekeeping)要留够:不绑核的 workqueue、kthread、定时器会被迁到它们身上,卸载出来的 RCU 回调也在那里执行。所以隔离的前提是至少留一个非隔离核;NUMA 机器上每个节点都该留,否则被迁走的工作要跨节点访问内存。
二、isolcpus:把核心移出调度域
isolcpus 是内核启动参数,写在 GRUB 命令行里,作用是把指定的核从调度器的负载均衡域中摘掉。
isolcpus=2-31
加上之后,调度器在做负载均衡时不会再把可运行任务往这些核上放。系统启动完成后,普通进程只会落在 0、1 两个核上。
它不阻止显式绑核。 这一点常被误解:isolcpus 隔离的是调度器的自动行为,而 sched_setaffinity(也就是 taskset 和上一篇的 core_affinity::set_for_current)设置的是显式亲和性,依然能把线程绑到隔离核上——这正是我们要的用法:核心默认没人用,只有你手动绑上去的线程在跑。
“
顺带提一个差异:
numactl --physcpubind会在下发前做一次可用性预检,隔离核不在默认掩码里,因此绑不上去。要绑隔离核就用taskset或程序内直接调sched_setaffinity。上一篇提到的”不要用get_core_ids()返回的列表去索引核心号”,也是同一个原因——那个列表只包含当前进程亲和性掩码内的核,隔离核不在里面。
它做不到的事。isolcpus 只管调度器负载均衡这一件事。时钟中断照发、RCU 回调照跑、硬件中断照打、per-CPU 内核线程照在。只配了 isolcpus 就以为核心干净了,是最常见的错误:延迟分布看着很好,但每隔 4ms 准时冒一个尖峰,那就是 tick。
2.1 两个 flag
isolcpus 支持在核心列表前加 flag:
isolcpus=domain,managed_irq,2-31
domain:从调度域中移除,也就是上面讲的默认行为。不写 flag 时等价于只有domain。managed_irq:让内核托管的中断尽量避开隔离核。
managed_irq 解决的是一个具体问题。像 NVMe 这样的多队列设备,它的中断是由内核 blk-mq 层托管的(managed interrupt),亲和性由内核按队列和核心的对应关系分配,用户态改不动。后面第五节会看到,手工往 /proc/irq/<n>/smp_affinity_list 里写 NVMe 队列的中断会直接失败。加上 managed_irq 之后,内核在为这类中断挑选处理核时会优先选非隔离核;只有当隔离核上确实有该设备的 I/O 在飞时,中断才会落回来。策略线程本来就不该在热路径上碰磁盘,所以这个条件基本不会触发。
2.2 它被认为是”过时”的
isolcpus 是启动参数,改一次要重启一次,运行时没法调整。因此内核社区一直建议改用 cpuset。较新的内核里,cgroup v2 的 cpuset 控制器提供了等价能力:
# 挂载在 /sys/fs/cgroup 的 cgroup v2 下
echo "+cpuset" > /sys/fs/cgroup/cgroup.subtree_control
mkdir /sys/fs/cgroup/hft
echo "2-31" > /sys/fs/cgroup/hft/cpuset.cpus
echo "isolated" > /sys/fs/cgroup/hft/cpuset.cpus.partition
cpuset.cpus.partition 写 isolated,就动态地实现了 isolcpus=domain 的效果,而且不用重启、随时可改。较新内核还进一步把隔离核从 unbound workqueue 的 cpumask 里摘掉,干扰面比启动参数版本更小。
不过对交易机这种配置基本不变、开机即定型的场景,启动参数一次配好反而更省心,出问题时 cat /proc/cmdline 一眼就能看到全部配置。下文按启动参数的方式走。
三、nohz_full:关掉时钟中断
nohz_full 开启的是”完全无 tick(full dynticks)”模式:当一个核上只有一个可运行任务时,内核停止给它发本地时钟中断。
nohz_full=2-31
要理解它的约束,得先明白 tick 是干什么的。tick 的主要职责是决定”当前任务是不是该被换下去”。如果核上只有一个可运行任务,这个判断没有意义——换下去也没别人可换,那就可以不发。这就是”单任务才停 tick”的由来。
由此引出几条硬约束:
一个隔离核上只放一个线程。 只要该核的运行队列里出现第二个可运行任务,tick 立刻恢复。所以核心布局要做到一核一线程,别图省事把策略线程和统计线程塞一个核上——那样 nohz_full 等于没配。
热路径上不要进内核。nohz_full 让内核的进入/退出路径变贵:为了做无 tick 的时间记账,每次用户态和内核态切换都要读时钟并做完整内存序的原子操作。原本几百纳秒的系统调用会更贵。热路径上的日志、read/write、缺页、动态内存分配都应该提前消化掉——预分配、mlockall 锁住内存、日志走无锁队列扔给非隔离核去写。
需要稳定的时钟源。 x86 上要求 TSC 可靠(constant_tsc、nonstop_tsc),否则内核会拒绝进入 full dynticks 模式。
至少要留一个非隔离核。 全部核都设成 nohz_full 是不允许的,内核会保留 CPU 0。被赶出去的定时器、workqueue、RCU 回调都要有地方跑。
还有 1Hz 的残留 tick。 调度器的部分统计量仍需周期性更新,内核保留了每秒一次的残留 tick。较新的内核已经把这部分卸载到非隔离核上远程执行,配置干净的情况下隔离核可以做到完全无 tick。
3.1 rcu_nocbs 是自动的
常见的写法是三个参数一起配:
isolcpus=2-31 nohz_full=2-31 rcu_nocbs=2-31
rcu_nocbs 的作用是把 RCU 回调从本地软中断挪到不绑核的 rcuo 内核线程里执行,这些线程只会跑在非隔离核上。
为什么 RCU 需要一个专门的开关,而 workqueue、kthread 不需要?这里有个区分:不绑核的工作(unbound workqueue、unbound kthread、普通定时器)内核可以直接迁走,隔离参数一开就自动迁到非隔离核;绑核的工作没这么简单,它按设计就必须在本地核执行,只能针对性地做一套卸载机制或者干脆关掉这个功能。RCU 回调属于后者,对应的机制就是 NOCB 模式。
但这里有个细节:参数 nohz_full 已经隐含了 rcu_nocbs。 内核在解析 nohz_full 时会自动把这些核加入 NOCB 集合——无 tick 的核根本没法驱动本地 RCU 状态机,不卸载回调这套就跑不起来。所以第三个参数是冗余的。写上无害,也便于配置自解释,但不必以为它是漏了就不行的一环。
四、写进 GRUB 并验证
把上面几项合到一起:
sudo vim /etc/default/grub
在 GRUB_CMDLINE_LINUX_DEFAULT 行末追加(32 核机器,留 0-1 做非隔离核):
isolcpus=domain,managed_irq,2-31 nohz_full=2-31 rcu_nocbs=2-31 irqaffinity=0-1
比前面讨论过的多了一项 irqaffinity=0-1:它设置的是中断的默认亲和性掩码。第五节会讲运行时怎么改 IRQ 亲和性,但那套脚本要等系统起来才能跑,在那之前的整个启动过程中,中断该往哪打没人管。irqaffinity 从第一个中断开始就把默认落点定在非隔离核上,把这段窗口一起堵上。
生成配置并重启:
# CentOS / RHEL / Amazon Linux
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
# Ubuntu / Debian
# sudo update-grub
sudo reboot
4.1 参数是否生效
第一步永远是确认内核确实收到了参数——GRUB 配置写错、grub2-mkconfig 忘了跑,都会让你在一台完全没隔离的机器上做半天优化:
$ cat /proc/cmdline
BOOT_IMAGE=... quiet isolcpus=2-31 nohz_full=2-31 rcu_nocbs=2-31
但 /proc/cmdline 只说明参数传进去了,不代表内核接受了。真正的确认要看 sysfs 里内核自己吐出来的隔离集合:
$ cat /sys/devices/system/cpu/isolated
2-31
$ cat /sys/devices/system/cpu/nohz_full
2-31
这两个文件是内核解析后的结果。如果 /proc/cmdline 里写了而这里是空的,说明参数被拒(拼写错误、核心号超范围、或内核没编译对应支持)。
4.2 进程是否真的被赶走了
htop 是最直观的一眼。按上一篇的方法打开 CPU 列,隔离生效后所有系统进程的这一列都应该只显示 0 或 1;顶部的每核负载条上,2 号以后的核应该是齐刷刷的空。
隔离生效后系统进程集中在 0-1 核
脚本里可以用 ps 做同样的检查——列出所有落在隔离核上的线程,正常情况下只应该出现 per-CPU 内核线程和你自己绑上去的那几个:
# 打印所有线程的所在核,筛出 CPU >= 2 的
ps -eLo psr,tid,comm --no-headers | awk '$1 >= 2'
这里会看到一批赶不走的名字:migration/N、ksoftirqd/N、cpuhp/N、idle_inject/N。这些是 per-CPU 内核线程,每个核一份,它们的亲和性由内核固定,隔离参数不会也不该移走它们。它们平时不占 CPU,只在有对应工作时才被唤醒——只要看到它们的 CPU 占用是 0,就不用管。
4.3 tick 是否真的停了
这是最容易被跳过、也最能说明问题的一项。/proc/interrupts 的 LOC 行是每个核收到的本地时钟中断计数,隔了一段时间对比两次,隔离核上的增量应该接近于零:
$ grep LOC /proc/interrupts
LOC: 4821334 4790012 1204 1198 ... Local timer interrupts
更精确的做法是用 perf 直接数隔离核上的时钟中断事件:
# 在 CPU 3 上采样 10 秒,统计本地时钟中断次数
$ sudo perf stat -e irq_vectors:local_timer_entry -C 3 -- sleep 10
判读方式:如果这个核上什么都没跑(空闲),本来就没有 tick,看不出问题;要在策略线程跑起来、绑上这个核之后再测。配置正确时,10 秒内应该是个位数到十几次(残留 tick);如果看到几千次,说明 nohz_full 没起作用——最常见的原因就是这个核上不止一个可运行任务。
4.4 直接量化剩余噪声
前面几项是逐个开关地查,还有一种自上而下的查法:不问哪个开关没生效,直接测这个核上到底还剩多少干扰。内核自带的 rtla(Real-Time Linux Analysis)工具集里的 osnoise 就是干这个的——它在目标核上跑一个占满 CPU 的采样线程,记录时间线里被别人抢走的每一段,最后按来源归类。
# 在 CPU 3 上测 10 秒
$ sudo rtla osnoise top -c 3 -d 10s
输出会给出这个核的可用率(干净配置下应该非常接近 100%),以及被硬件中断、不可屏蔽中断、软中断、线程切换各自吃掉多少微秒、发生多少次。这比逐项核对更能说明问题:数值不达标时,分类计数会直接告诉你漏的是哪一类,回头再去查对应的那个开关。
rtla 随内核源码发布,多数发行版打在 rtla 或内核工具包里,装不上时用 perf 那一套逐项查也够用。
五、把硬件中断挪走
启动参数里的 irqaffinity 定的是默认掩码,只对沿用默认值的中断有效。驱动加载时自己指定过亲和性的中断不受它管,irqbalance 起来之后更是会把一切推倒重来。所以运行时还得再收一遍。对吃行情的机器来说这一步不能省——网卡中断恰恰是频率最高的那一类。
5.1 关掉 irqbalance
irqbalance 是个守护进程,会周期性地重新分配中断亲和性。不关掉它,你手工设的亲和性过一会儿就被改回去:
sudo systemctl stop irqbalance
sudo systemctl disable irqbalance
5.2 把中断绑到非隔离核
遍历所有中断,把亲和性设成 0-1:
for irq in /proc/irq/*/; do
echo 0-1 > "${irq}smp_affinity_list" 2>/dev/null
done
写 smp_affinity_list 比写 smp_affinity 方便,前者接受 0-1 这样的列表,后者要手算十六进制掩码。
有一部分会写失败,这是正常的:
fail irq 68
fail irq 69
失败的是内核托管的中断——NVMe 的各个 I/O 队列就属于这一类,它们的亲和性由 blk-mq 按队列绑定,用户态无权修改。这正是第二节里 isolcpus=managed_irq,... 要解决的问题:既然用户态改不了,就让内核在分配时自己避开隔离核。
5.3 核对结果
/proc/irq/<n>/smp_affinity_list 是”请求的”亲和性,effective_affinity_list 是内核实际生效的。两者可能不一致(比如中断控制器只支持单核投递时,内核会从掩码里挑一个),要看的是后者:
$ for irq in $(ls /proc/irq | grep -E '^[0-9]+$'); do
name=$(awk -v i="$irq:" '$1==i{print $NF}' /proc/interrupts)
[ -z "$name" ] && continue
printf "IRQ %-3s %-26s req=%-8s eff=%-8s\n" "$irq" "$name" \
"$(cat /proc/irq/$irq/smp_affinity_list 2>/dev/null)" \
"$(cat /proc/irq/$irq/effective_affinity_list 2>/dev/null)"
done
IRQ 67 nvme0q0 req=0-1 eff=14
IRQ 68 nvme0q1 req=0-15 eff=15
IRQ 69 nvme0q2 req=16-31 eff=31
IRQ 70 ena-mgmnt@pci:0000:27:00.0 req=0-1 eff=1
IRQ 71 enp39s0-Tx-Rx-0 req=0-1 eff=0
IRQ 72 enp39s0-Tx-Rx-1 req=0-1 eff=0
IRQ 73 enp39s0-Tx-Rx-2 req=0-1 eff=1
IRQ 74 enp39s0-Tx-Rx-3 req=0-1 eff=1
IRQ 75 enp39s0-Tx-Rx-4 req=0-1 eff=1
IRQ 76 enp39s0-Tx-Rx-5 req=0-1 eff=0
IRQ 77 enp39s0-Tx-Rx-6 req=0-1 eff=0
IRQ 78 enp39s0-Tx-Rx-7 req=0-1 eff=1
网卡(enp39s0)的 8 个收发队列中断全部落在 0-1,符合预期。NVMe 的三个队列改不动,实际生效在 CPU 14、15、31 上。
5.4 按实际中断分布选核
改不动就绕开。看一眼各设备的中断实际打在哪些核上,选策略核时躲开它们:
egrep "ena|eth|ens|enp|nvme|virtio|xen|blk" /proc/interrupts | awk '{
printf "%-25s ", $NF;
for (i = 2; i <= NF; i++) {
# 遇到非纯数字列(PCI-MSI 等描述部分)就停
if ($i !~ /^[0-9]+$/) break;
if ($i > 0) printf "CPU%-2d: %-12s ", i-2, $i;
}
print "";
}'
在一台 32 核机器上跑出来是这样:
nvme0q0 CPU0 : 17 CPU14: 17
nvme0q1 CPU15: 27006387
nvme0q2 CPU31: 775242
ena-mgmnt@pci:0000:27:00.0 CPU0 : 654 CPU1 : 4835932
enp39s0-Tx-Rx-0 CPU0 : 2120287246
enp39s0-Tx-Rx-1 CPU0 : 2445503280
enp39s0-Tx-Rx-2 CPU1 : 2558002118
enp39s0-Tx-Rx-3 CPU1 : 1760950210
enp39s0-Tx-Rx-4 CPU1 : 1925551053
enp39s0-Tx-Rx-5 CPU0 : 2274043800
enp39s0-Tx-Rx-6 CPU0 : 1979077339
enp39s0-Tx-Rx-7 CPU1 : 2248068204
网卡中断全在 0-1,干净。NVMe 队列压在 14、15、31 上,其中 nvme0q1 已经累计了两千七百万次中断。结论很直接:策略线程避开 0、1、14、15、31,从 2-13、16-30 里挑。
六、几个容易踩的坑
配了参数但没绑核。 隔离和绑核是两件事,隔离只是把核腾空,你还得主动把线程绑上去。只配了 isolcpus 不改程序,结果是这些核完全闲置,整机可用算力凭空少了一大半。
一个隔离核塞多个线程。 前面讲过,这会让 nohz_full 直接失效。核心布局要一核一线程。
绑到了 SMT 的兄弟核。 如果没关超线程,逻辑核 3 和逻辑核 19 可能是同一个物理核的两个线程,把两个热路径线程分别绑上去等于让它们抢同一套执行单元。这也是上一篇之前先写 SMT 的原因——先关 SMT,后面所有的核心编号才是干净的物理核。
BIOS 或内核升级后配置回滚。 发行版升级内核时会重新生成 GRUB 配置,自定义参数一般会保留,但不是所有场景都保留。开机自检脚本里加一条 cat /sys/devices/system/cpu/isolated 的断言,比出事后再查省事。
只看 /proc/cmdline 就认为配好了。 前面说过,要看 sysfs 里内核解析后的结果,以及 perf 数出来的 tick 计数。
在隔离核上做系统调用。 这就是第一节说的同步噪声——配置管不了,只能靠改程序。nohz_full 下进出内核更贵,热路径上的每一次 write、每一次缺页、每一次 malloc 触发的 brk/mmap 都被放大。配套动作是启动时 mlockall(MCL_CURRENT | MCL_FUTURE) 锁内存、预分配所有缓冲区、预热代码路径、日志异步化。缺页尤其值得单拎出来:一次 major fault 要走磁盘,量级和你辛苦省下来的那些微秒完全不在一个尺度上。
云上环境的边界。 上面这套在 AWS 的 EC2 上完整可用——GRUB 能改、/proc/irq 能写、CPU 拓扑信息真实。但虚拟化层本身的干扰(宿主机调度、steal time)不在你的控制范围内,vmstat 的 st 列长期不为零就说明有邻居在抢。
七、小结
一条完整的隔离配置,和它各自负责的那类干扰:
| 手段 | 关掉的干扰 | 配置位置 |
| — | — | — |
| isolcpus=domain,... | 调度器把其它任务放上来 | GRUB |
| nohz_full=... | 周期性时钟中断 | GRUB |
| rcu_nocbs=... | 本地 RCU 回调(nohz_full 已隐含) | GRUB |
| isolcpus=managed_irq,... | 内核托管中断(NVMe 等) | GRUB |
| irqaffinity=... | 启动阶段的中断默认落点 | GRUB |
| 停 irqbalance + smp_affinity_list | 普通硬件中断(网卡等) | 运行时 |
| sched_setaffinity / taskset | 把线程放到腾空的核上 | 程序 / 运行时 |
对应的验证清单:
cat /sys/devices/system/cpu/isolated # 隔离集合被内核接受
cat /sys/devices/system/cpu/nohz_full # 无 tick 集合被内核接受
ps -eLo psr,tid,comm --no-headers | awk '$1 >= 2' # 隔离核上只剩内核线程和自己的线程
sudo perf stat -e irq_vectors:local_timer_entry -C 3 -- sleep 10 # tick 确实停了
cat /proc/irq/*/effective_affinity_list # 中断实际落在非隔离核
sudo rtla osnoise top -c 3 -d 10s # 该核剩余噪声的总量与分类
隔离这件事的特点是:每一项单独看收益都不大,漏掉任何一项都会在 p99.9 之后留下一个尖峰,而尖峰的周期性又很容易被当成”偶发抖动”忽略掉。所以配完一定要逐条验证,而不是配完重启看一眼 htop 就收工。
最后要说明的是,隔离只是把软件调度层面的干扰清干净了。
要让一个隔离核真正稳定,还有几件事要配合:关掉 SMT 避免超线程抢执行单元、管好 CPU 频率调节、限制深度 C-state等等。关 SMT 这个系列已经写过, CPU 电源管理——一个空转等行情的核,如果被降频或者进了深度睡眠,下一个包到达时唤醒本身就要几微秒。下一篇就聊这个:C-state、P-state 与频率调节,怎么让一个隔离核始终保持在随时可用的状态。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:内存泄漏 《高频交易低延迟优化:CPU隔离 isolcpus/nohz_full/RCU/IRQ》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论