WEBKT

混部场景下 Cgroup v2 cpu.weight 与 cpu.idle 协同压制离线业务的内核机理与实践

9 0 0 0

在企业级数据中心里,将延迟敏感的在线业务(Latency-Sensitive, LS)与吞吐量导向的离线业务(Best-Effort, BE)混合部署在同一台物理机上,是压榨 CPU 利用率的常用手段。然而,混部面对的最大技术挑战,是如何防止离线业务在 CPU 密集计算时抢占在线业务的 CPU 时间片,进而导致在线业务的 P99 延迟飙升。

传统的 Cgroup v1 主要依赖 cpu.shares(在 v2 中对应 cpu.weight)进行比例控制。但在高负载的极端场景下,单纯依靠权重分配并不能彻底解决离线业务对在线业务的干扰。随着 Linux 内核引入 Cgroup v2 的 cpu.idle 特性,两者协同配合为离线业务的“无感压制”提供了更优雅、更底层的解决方案。

为什么单一控制器无法完美解决混部干扰?

要理解协同的必要性,需要先剖析单一控制器在混部场景下的局限性。

1. 孤立使用 cpu.weight 的缺陷

在 Cgroup v2 中,cpu.weight(默认值为 100,范围 1-10000)决定了当 CPU 处于繁忙状态时,各 Cgroup 按比例分配 CPU 时间的额度。

假设在线 Cgroup 设置为 cpu.weight = 10000,离线 Cgroup 设置为最低的 cpu.weight = 1。看似在线业务拿到了 99.99% 的绝对优势,但在 CFS(Completely Fair Scheduler,完全公平调度器)的实际运行中,仍存在以下问题:

  • 抢占延迟(Preemption Latency):CFS 调度器为了保证“完全公平”,会为每个进程维护虚拟运行时间(vruntime)。即使离线进程的权重极低,其 vruntime 增长极快,但只要它被唤醒且当前 vruntime 仍小于在线进程,它依然有权利抢占在线进程,或者在时钟中断到来前消耗掉一个完整的最小运行粒度(sysctl_sched_min_granularity_ns)。这种微秒级的抢占,对毫秒级敏感的在线业务(如 Redis、搜索推荐)是致命的。
  • 惊群效应与上下文切换:当离线业务包含大量并发线程时,即使每个线程的权重极低,多个线程频繁唤醒、抢占再挂起的过程,也会产生巨大的上下文切换开销,并污染 CPU L1/L2 缓存。

2. 孤立使用 cpu.idle 的局限

Linux Kernel 5.14 引入了 cpu.idle 属性,允许将某个 Cgroup 标记为“空闲”组。当 cpu.idle = 1 时,该 Cgroup 内的所有任务都会以 SCHED_IDLE 调度策略运行。

  • 调度行为SCHED_IDLE 任务的优先级比普通 CFS 任务低得多。只要系统中有任何普通的 CFS 任务(即在线业务)处于 Runnable 状态,SCHED_IDLE 任务就会立即被无条件抢占,且绝对不会主动分走任何 CPU 时间。
  • 局限性:如果只开启 cpu.idle,在没有在线业务运行的间歇期,离线业务会迅速占满 CPU。此时,若有多个不同的离线业务(例如:大数据跑批任务与日志收集 Agent),它们在 SCHED_IDLE 级别内部无法进行精细化的优先级区分,会产生无序竞争。此外,在内核进行 CPU 负载均衡(Load Balancing)和 PELT(Productive Entity Load Tracking)负载计算时,如果离线组的权重依然保持默认值,内核可能会产生误判,将在线任务向外迁移,从而造成意料之外的性能抖动。

cpu.weightcpu.idle 的协同工作机理

cpu.weightcpu.idle 结合使用,不仅能在宏观上将离线业务降级为系统级“备胎”,还能在微观上规范离线业务内部的资源争夺,同时保护内核调度器的负载均衡不被污染。

                       [ CPU 物理核心 ]
                               │
            ┌──────────────────┴──────────────────┐
            ▼                                     ▼
   【 在线业务 Cgroup 】                  【 离线父组 Cgroup 】
   (cpu.idle = 0)                         (cpu.idle = 1)
   (cpu.weight = 10000)                   (cpu.weight = 1) <── 避免负载均衡误判
            │                                     │
     [运行优先级最高]                     [仅在 CPU 空闲时运行]
                                                  │
                                   ┌──────────────┴──────────────┐
                                   ▼                             ▼
                        【 离线子组 A (Spark) 】        【 离线子组 B (Log) 】
                        (cpu.weight = 800)             (cpu.weight = 200)
                                 └──────────────┬──────────────┘
                                                ▼
                                    [在离线内部按 8:2 分配空闲 CPU]

1. 规避内核负载均衡器的“幻影负载”

内核的 CFS 调度器在进行 CPU 间负载均衡时,会评估每个 CPU 运行队列(runqueue)的 Load 值,该值与队列中任务的 weight 强相关。
如果一个离线 Cgroup 包含 64 个活跃线程,即使它被设置了 cpu.idle = 1,如果其 cpu.weight 保持默认的 100(甚至被误设为更高的值),内核在计算该 CPU 的整体 Load 时,依然会认为这个 CPU 处于“重载”状态。这会导致:

  • 内核不愿将新唤醒的在线任务调度到这个 CPU 上(尽管该 CPU 上的离线任务可以被瞬间抢占)。
  • 内核会尝试将这个 CPU 上的其他非 idle 任务迁移走。

协同机制:将离线 Cgroup 的 cpu.weight 设为最小值 1,同时设置 cpu.idle = 1。这样既告诉调度器“这个组的任务只有在绝对空闲时才能跑”,又告诉负载均衡器“这个组的整体负载权重极低,在做多核负载均衡计算时可以忽略不计”,从而彻底消除了离线任务对在线任务调度路径的隐性干扰。

2. 离线业务内部的梯队化管理

在实际生产中,离线业务并非单一实体。通常存在“高价值离线”(如 AI 模型离线推理、数仓 ETL)与“低价值离线”(如冷数据备份、日志压缩)。

通过 Cgroup v2 的层级结构,我们可以这样设计:

  1. 创建一个统一的离线父 Cgroup:offline/
    • 设置 offline/cpu.idle = 1
    • 设置 offline/cpu.weight = 1
  2. offline/ 下创建子 Cgroup:
    • 核心离线子组:offline/high_priority/,设置 cpu.weight = 800
    • 次要离线子组:offline/low_priority/,设置 cpu.weight = 200

运行效果

  • 当在线业务需要 CPU 时,整个 offline/ 树下的所有任务瞬间让出 CPU,不产生可感知的抢占延迟。
  • 当在线业务挂起、CPU 出现空闲时,offline/ 获得执行机会。此时,high_prioritylow_priority 两个子组将严格按照 8:2 的比例瓜分这些空闲的 CPU 时间,避免了日志收集等低价值任务反向抢占数仓计算任务的情况。

落地实践配置

以下是在现代 Linux 发行版(内核版本 $\ge$ 5.14,且已启用 Cgroup v2)下的实操配置步骤。

Step 1: 验证 Cgroup v2 挂载与控制器

首先,确保系统使用的是 Cgroup v2,并且 cpu 控制器在子树中已被启用。

# 检查是否为 Cgroup v2(输出应为 cgroup2fs 或 cgroup2)
stat -f /sys/fs/cgroup

# 检查根目录下是否启用了 cpu 控制器
cat /sys/fs/cgroup/cgroup.subtree_control
# 如果没有 cpu,执行以下命令启用
echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control

Step 2: 创建在线与离线 Cgroup 目录树

# 创建在线业务组
mkdir -p /sys/fs/cgroup/online

# 创建离线父组及子组
mkdir -p /sys/fs/cgroup/offline/high_priority
mkdir -p /sys/fs/cgroup/offline/low_priority

# 启用子树的 cpu 控制器
echo "+cpu" > /sys/fs/cgroup/offline/cgroup.subtree_control

Step 3: 配置参数进行协同压制

配置在线业务高权重、非 idle:

echo 10000 > /sys/fs/cgroup/online/cpu.weight
echo 0 > /sys/fs/cgroup/online/cpu.idle

配置离线父组为 最低权重 + 绝对空闲

echo 1 > /sys/fs/cgroup/offline/cpu.weight
echo 1 > /sys/fs/cgroup/offline/cpu.idle

配置离线子组的内部竞争比例(8:2):

echo 800 > /sys/fs/cgroup/offline/high_priority/cpu.weight
echo 200 > /sys/fs/cgroup/offline/low_priority/cpu.weight

Step 4: 将进程分类写入对应的 group

将在线服务的 PID 写入 /sys/fs/cgroup/online/cgroup.procs;将离线任务(如 Spark Executor 进程)写入 /sys/fs/cgroup/offline/high_priority/cgroup.procs


性能观测与成效评估

应用此配置后,可以通过以下几个维度来观测协同压制的效果:

1. 抢占延迟与抖动观测

使用 bpftracebcc-tools 中的 runqlat 工具,观测在线业务进程在 CPU 运行队列中的等待时间(Run Queue Latency)。
在未开启 cpu.idle 协同前,由于低权重离线任务的抢占,在线业务的 runqlat 可能会出现偶发性的 10ms 以上的毛刺;协同开启后,runqlat 会稳定在微秒(us)级别,毛刺基本消失。

2. PSI(Pressure Stall Information)指标分析

监控 /proc/pressure/cpu。该指标反映了系统由于 CPU 资源不足而导致的任务阻塞情况。

cat /proc/pressure/cpu

在协同模式下,在线业务即使在整机 CPU 利用率达到 90% 以上时,其 somefull 指标也应接近 0,而离线 Cgroup 内部的 PSI 指标可能会走高。这说明压力被成功“限制”并“隔离”在了离线组内部。

3. 业务指标验证

最直观的验证是在线业务的 P99 延迟。在物理机整机 CPU 使用率从 30%(仅在线跑)提升到 85%(混部离线跑)的过程中,在线业务的 P99 响应时间波动通常可以控制在 2% 以内,基本实现“无感混部”。

总结

在 Cgroup v2 框架下,cpu.weightcpu.idle 并非孤立的控制维度。将 cpu.idle = 1 作为离线业务的“大闸”,使其退出与在线业务的直接竞争;同时将 cpu.weight = 1 作为离线父组的“隔离带”,防止内核负载均衡失效;最后在离线内部利用子组 cpu.weight 进行秩序维护。这种立体化的配置方案,才是当前 Linux 混部场景下保障服务质量(QoS)的最佳实践。

SysOps极客 Cgroup v2混部技术Linux内核调度

评论点评