混部场景下 Cgroup v2 cpu.weight 与 cpu.idle 协同压制离线业务的内核机理与实践
在企业级数据中心里,将延迟敏感的在线业务(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.weight 与 cpu.idle 的协同工作机理
将 cpu.weight 与 cpu.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 的层级结构,我们可以这样设计:
- 创建一个统一的离线父 Cgroup:
offline/- 设置
offline/cpu.idle = 1 - 设置
offline/cpu.weight = 1
- 设置
- 在
offline/下创建子 Cgroup:- 核心离线子组:
offline/high_priority/,设置cpu.weight = 800 - 次要离线子组:
offline/low_priority/,设置cpu.weight = 200
- 核心离线子组:
运行效果:
- 当在线业务需要 CPU 时,整个
offline/树下的所有任务瞬间让出 CPU,不产生可感知的抢占延迟。 - 当在线业务挂起、CPU 出现空闲时,
offline/获得执行机会。此时,high_priority与low_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. 抢占延迟与抖动观测
使用 bpftrace 或 bcc-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% 以上时,其 some 和 full 指标也应接近 0,而离线 Cgroup 内部的 PSI 指标可能会走高。这说明压力被成功“限制”并“隔离”在了离线组内部。
3. 业务指标验证
最直观的验证是在线业务的 P99 延迟。在物理机整机 CPU 使用率从 30%(仅在线跑)提升到 85%(混部离线跑)的过程中,在线业务的 P99 响应时间波动通常可以控制在 2% 以内,基本实现“无感混部”。
总结
在 Cgroup v2 框架下,cpu.weight 与 cpu.idle 并非孤立的控制维度。将 cpu.idle = 1 作为离线业务的“大闸”,使其退出与在线业务的直接竞争;同时将 cpu.weight = 1 作为离线父组的“隔离带”,防止内核负载均衡失效;最后在离线内部利用子组 cpu.weight 进行秩序维护。这种立体化的配置方案,才是当前 Linux 混部场景下保障服务质量(QoS)的最佳实践。