CPU 隔离

简介

“CPU 隔离”是指让一个 CPU 专属于给定的工作负载,而没有任何来自内核的不需要的代码干扰。

这些干扰通常被称为“噪声”,可以由异步事件(中断、定时器、工作队列和内核线程导致的调度器抢占等)或同步事件(系统调用和缺页中断)触发。

这种噪声通常不会被注意到。毕竟,同步事件是所请求的内核服务的一个组成部分;而异步事件要么在作为任务执行时被调度器充分且均匀地分发,要么在作为中断执行时足够快。定时器中断甚至可以每秒执行 1024 次,且在大多数情况下不会产生显著且可测量的影响。

然而,一些罕见且极端的工作负载对这类噪声可能非常敏感。例如,无法承受丢失单个数据包的高带宽网络处理,或者极低延迟的网络处理。通常,这些用例涉及 DPDK,它绕过了内核网络栈,并从用户空间直接访问网络设备。

为了在没有或只有有限内核噪声的情况下运行 CPU,需要将相关的主控(housekeeping)工作关闭、迁移或卸载。

主控(Housekeeping)

在 CPU 隔离术语中,主控是指内核为了维持其所有服务而需要处理的工作,通常是异步的。它对应于上面列出的噪声和干扰,除非至少有一个 CPU 被隔离。如果必须卸载绑定到 CPU 的工作,主控可能会采用进一步的应对机制。

主控 CPU 是指将内核噪声从隔离 CPU 移开的非隔离 CPU。

根据噪声的性质,可以通过多种方式实现隔离:

  • 未绑定工作(“未绑定”意为不绑定到任何 CPU)可以简单地从隔离 CPU 迁移到主控 CPU。无绑定的工作队列、内核线程和定时器就是这种情况。

  • 绑定工作(“绑定”意为绑定到特定的 CPU)本质上通常无法直接移走。要么:

    • 该工作必须切换到加锁的实现。例如:带有 CONFIG_RCU_NOCB_CPU 的 RCU 就是这种情况。

    • 相关功能必须关闭,并被认为与隔离的 CPU 不兼容。例如:锁死监视器、不可靠的时钟源等。

    • 采用精心设计且沉重的应对机制作为替代。例如:在 nohz_full CPU 上关闭定时器时钟节拍,但约束它们只能运行单个任务。这在内核进/出时增加了显著的成本代价,并将残余的 1Hz 调度器时钟节拍卸载到主控 CPU。

无论如何,主控工作都必须得到处理,这就是为什么系统中必须至少有一个主控 CPU,如果机器运行很多 CPU,最好有更多。例如在 NUMA 系统上每个节点一个。

此外,CPU 隔离通常意味着在无噪声的隔离 CPU 与主控 CPU 上增加的开销(有时甚至包括进入内核的隔离 CPU)之间进行权衡。

隔离功能

可以在内核中配置不同级别的隔离,每种级别都有其自身的缺点和权衡。

调度器域隔离

此功能将 CPU 从调度器拓扑中隔离。因此,目标 CPU 不再是负载均衡的一部分。除非显式指定亲和性,否则任务不会从该 CPU 迁移出去,也不会迁移进来。

作为副作用,该 CPU 也从无绑定工作队列和无绑定内核线程中被隔离。

要求

  • 针对基于 cpusets 的接口需要 CONFIG_CPUSETS=y

权衡

由于某些 CPU 被从全局负载均衡中剥离,系统负载整体上的分布自然会减少。

接口

  • Control Group v2 cpuset 隔离分区,因为它们可以在运行时进行调整。

  • 带有 ‘domain’ 标志的内核启动参数 ‘isolcpus=’ 是一种不够灵活的替代方案,它不允许运行时重新配置。

中断(IRQs)隔离

尽可能隔离中断,使它们不在目标 CPU 上触发。

接口

  • /proc/irq/*/smp_affinity 文件,详见 SMP IRQ affinity 页面。

  • 用于默认设置的内核启动参数 “irqaffinity=”。

  • “isolcpus=” 内核启动参数中的 “managed_irq” 标志会针对托管中断尝试尽最大努力的亲和性覆盖。

完全动态时钟节拍(Full Dynticks,又称 nohz_full)

完全动态时钟节拍将动态时钟空闲模式(在 CPU 空闲时停止时钟节拍)扩展到了在用户空间运行单个任务的 CPU。也就是说,如果环境允许,定时器时钟节拍将被停止。

全局定时器回调也从 nohz_full CPU 中隔离出来。

要求

  • CONFIG_NO_HZ_FULL=y

约束 (Constraints)

  • 隔离的 CPU 必须仅运行单个任务。多任务需要时钟节拍来维持抢占。这通常没问题,因为工作负载通常无法忍受随机上下文切换带来的延迟。

  • 隔离的 CPU 不能进行内核调用,否则会有触发随机噪声的风险。

  • 不能在隔离的 CPU 上使用 POSIX CPU 定时器。

  • 架构必须具有稳定可靠的时钟源(不能是没有 watchdog 就不可靠的 TSC)。

权衡

就成本而言,这是最具侵入性的隔离功能。它假定在工作负载将其大部分时间花在用户空间且除了准备工作之外不依赖内核时使用,因为

  • 由于加锁、卸载以及线程化的回调处理,RCU 增加了更多开销(这与通过 “rcu_nocbs” 启动参数获得的效果相同)。

  • 通过系统调用、异常和中断进行内核进/出操作成本更高,因为需要完全排序的 RmW 操作来将用户空间维护为 RCU 扩展静止态。此外,CPU 时间是在内核边界处进行统计,而不是从时钟节拍中定期统计。

  • 主控 CPU 必须代表隔离的 CPU 运行 1Hz 的残余远程调度器时钟节拍。

检查清单

您已经设置了上述各项隔离功能,但仍然观察到破坏工作负载的抖动?在继续之前,请务必检查以下几个要素。

其中一些检查清单项目与实时工作负载的项目类似:

  • 使用 mlock() 防止您的页面被换出。缺页中断通常与对抖动敏感的工作负载不兼容。

  • 避免使用 SMT,以防止您的硬件线程被另一个硬件线程“抢占”。

  • CPU 频率变化可能会在工作负载中引起微妙的抖动。应当谨慎使用和调整 Cpufreq。

  • 深层 C 状态可能会导致唤醒时的延迟问题。如果这成为一个问题,可以通过内核启动参数(如 processor.max_cstate 或 intel_idle.max_cstate)来限制 C 状态。更细粒度的调优说明请参见 CPU Idle Time Management 页面

  • 您的系统可能会受到来自固件的中断的影响——例如 x86 具有系统管理中断(SMIs)。检查您的系统 BIOS 以禁用此类干扰,如果运气好的话,您的供应商会提供用于低延迟操作的 BIOS 调优指南。

完全隔离示例

在此示例中,系统有 8 个 CPU,第 8 个 CPU 将被完全隔离。由于 CPU 从 0 开始编号,因此第 8 个 CPU 是 CPU 7。

内核参数

设置以下内核启动参数以禁用 SMT 并设置时钟节拍和中断隔离

  • 完全动态时钟节拍:nohz_full=7

  • 中断隔离:irqaffinity=0-6

  • 托管中断隔离:isolcpus=managed_irq,7

  • 防止 SMT:nosmt

完整的命令行随后为

nohz_full=7 irqaffinity=0-6 isolcpus=managed_irq,7 nosmt

CPUSET 配置(cgroup v2)

假定 cgroup v2 已挂载到 /sys/fs/cgroup,以下脚本将 CPU 7 从调度器域中隔离。

cd /sys/fs/cgroup
# Activate the cpuset subsystem
echo +cpuset > cgroup.subtree_control
# Create partition to be isolated
mkdir test
cd test
echo +cpuset > cgroup.subtree_control
# Isolate CPU 7
echo 7 > cpuset.cpus
echo "isolated" > cpuset.cpus.partition

用户空间工作负载

模拟一个纯用户空间工作负载,下面的程序在隔离的 CPU 7 上运行一个虚拟的用户空间循环。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(void)
{
    // Move the current task to the isolated cpuset (bind to CPU 7)
    int fd = open("/sys/fs/cgroup/test/cgroup.procs", O_WRONLY);
    if (fd < 0) {
        perror("Can't open cpuset file...\n");
        return 0;
    }

    write(fd, "0\n", 2);
    close(fd);

    // Run an endless dummy loop until the launcher kills us
    while (1)
    ;

    return 0;
}

编译它并保存以用于后续步骤

# gcc user_loop.c -o user_loop

启动器

下面的启动器运行上述程序 10 秒钟,并追踪由抢占任务和中断产生的噪声。

TRACING=/sys/kernel/tracing/
# Make sure tracing is off for now
echo 0 > $TRACING/tracing_on
# Flush previous traces
echo > $TRACING/trace
# Record disturbance from other tasks
echo 1 > $TRACING/events/sched/sched_switch/enable
# Record disturbance from interrupts
echo 1 > $TRACING/events/irq_vectors/enable
# Now we can start tracing
echo 1 > $TRACING/tracing_on
# Run the dummy user_loop for 10 seconds on CPU 7
./user_loop &
USER_LOOP_PID=$!
sleep 10
kill $USER_LOOP_PID
# Disable tracing and save traces from CPU 7 in a file
echo 0 > $TRACING/tracing_on
cat $TRACING/per_cpu/cpu7/trace > trace.7

如果没有出现特定问题,trace.7 的输出应该类似于以下内容

<idle>-0 [007] d..2. 1980.976624: sched_switch: prev_comm=swapper/7 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=user_loop next_pid=1553 next_prio=120
user_loop-1553 [007] d.h.. 1990.946593: reschedule_entry: vector=253
user_loop-1553 [007] d.h.. 1990.946593: reschedule_exit: vector=253

也就是说,在 user_loop 运行的 10 秒钟内,在第一次追踪和第二次追踪之间没有触发特定的噪声。

调试

当然,事情从来没有这么简单,尤其是在这方面。很有可能会在上述 trace.7 文件中观察到实际的噪声。

进一步调查的最佳方法是启用更细粒度的追踪点,例如生成异步事件的子系统的追踪点:workqueue、timer、irq_vector 等。启用 tick_stop 事件以诊断为什么在发生这种情况时保留了时钟节拍,也会很有趣。

某些工具对于更高层次的分析也可能有用

  • rtla 提供了一套用于分析系统中延迟和噪声的工具。例如 rtla-osnoise 运行一个内核追踪器,用于分析噪声并输出摘要。

  • dynticks-testing 做的事情类似于 rtla-osnoise,但在用户空间中。它位于 git://git.kernel.org/pub/scm/linux/kernel/git/frederic/dynticks-testing.git