Linux进程调度与负载:从CFS到EEVDF与load average

举报
yd_236430038 发表于 2026/11/11 23:54:14 2026/11/11
【摘要】 Linux进程调度与负载:从CFS到EEVDF与load average虚拟内存解决"能不能装下",调度解决"谁在什么时候拿到 CPU、拿多久"。本文讲三件事:调度器如何决定下一个运行者(CFS 与 6.6 之后的 EEVDF)、cgroup 的权重与配额为什么会让容器莫名被卡、以及 load average 为什么和 CPU 使用率经常给出相反结论。 调度器要同时满足的四件事公平 ...

Linux进程调度与负载:从CFS到EEVDF与load average

虚拟内存解决"能不能装下",调度解决"谁在什么时候拿到 CPU、拿多久"。本文讲三件事:调度器如何决定下一个运行者(CFS 与 6.6 之后的 EEVDF)、cgroup 的权重与配额为什么会让容器莫名被卡、以及 load average 为什么和 CPU 使用率经常给出相反结论。

调度器要同时满足的四件事

公平     每个任务按权重获得大致成比例的 CPU 时间
响应     交互式任务(键盘、渲染)不能被批处理任务拖住
吞吐     上下文切换本身是纯开销,切得越勤吞吐越低
实时     硬deadline 任务必须有上界保证

这四项互相冲突,Linux 的取舍是:默认完全公平,实时靠优先级抢占,响应性靠睡眠惩罚补偿。理解了这一点,下面所有机制都是它的推论。

调度类与优先级层次

内核不是用一个算法管所有任务,而是按"调度类"分层,优先级高的类有任务时低类直接没份:

stop  →  deadline  →  realtime  →  fair  →  idle
(迁移/暂停用) (SCHED_DEADLINE) (SCHED_FIFO/RR) (普通进程) (SCHED_IDLE)
策略 属于哪类 时间片 抢占 典型用途
SCHED_OTHER fair 由 CFS/EEVDF 动态决定 可被更高类抢 绝大多数进程
SCHED_BATCH fair 视为"不交互",不唤醒优化 同上 编译、离线任务
SCHED_IDLE idle 只有机器全空才跑 被任何人抢 后台清理、低优先守护
SCHED_FIFO realtime 不用完不切(除非被抢/让出) 同级不轮转 音频、控制回路
SCHED_RR realtime 同级按片轮转 同上 实时计算密集型
SCHED_DEADLINE deadline 声明 runtime/period/deadline 准入控制 需要带宽保证的任务

nice 值到权重的映射是指数的:

weight = approx_constant / exp2(nice)      (6.6 前)
每 +1 nice → 权重约降到 0.8 倍(约 82%)
nice 0 与 nice 19 的 CPU 份额相差约 60 倍

这意味着用 nice 压制一个进程比想象的强得多,而差 1 个 nice 几乎感觉不到。

CFS:用vruntime 把"公平"变成一个可比较的数

CFS 没有固定时间片,它维护一个虚拟运行时间:

vruntime += 实际运行时间 × (NICE_0_LOAD / 当前权重)
  • 权重高(nice 小)→ 同样跑 10ms,vruntime 增长少 → 还能继续跑
  • 权重低(nice 大)→ vruntime 涨得快 → 很快就"落后于"别人,被换下

调度决策就一句话:红黑树上 vruntime 最小的可运行任务上位。

        runqueue(每个 CPU 一个)
        红黑树按 vruntime 排序
             ├─ 最左节点 = 下一个运行的任务(缓存起来,O(1) 取)
             └─ 新任务插入位置:max_vruntime 附近(避免新任务霸占 CPU)

        睡眠唤醒:vruntime 拉到 min_vruntime 附近
        → 短暂睡眠的任务获得补偿,能立刻再跑(交互性来源)
        → 长时间睡眠的任务补偿更多(可能立刻抢占当前任务,是延迟毛刺的一个来源)

CFS 的痛点:vruntime 只保证累积公平,不保证单轮的响应上界。系统里可运行任务数波动时,某个任务的等待时间没有可预测的界,音视频、游戏这类任务会感到偶发卡顿。这是 EEVDF 要解决的问题。

EEVDF(6.6 起替代 CFS):给每个任务一个到期时间

EEVDF 的核心是"负延迟"概念:任务不应得的时间(lag)为负时才有资格运行。

每个任务两个量:
  request    这一轮想要多长(默认按历史 slice 推算)
  deadline   希望什么时候完成 = 现在 + 按权重折算的等待

两步决策:
① 先筛出 eligible 的任务(累计 lag ≤ 0,即它确实"欠"CPU)
② 在 eligible 里挑 deadline 最早的运行

效果对比:

维度 CFS EEVDF
选择依据 vruntime 最小 eligible 中 deadline 最早
单次等待上界 无明确保证 与权重、request 挂钩,可预测
大时间片任务 一旦开跑长时间不让出 可被要求让出,响应更好
调优入口 sched_min_granularity_ns 等 sysctl 主要通过 sched_start_period、request 机制

升级内核后调度延迟分布变好、但某些 CPU 密集型基准吞吐略降,是这个换代的正常表现。

时间片不是常数

传统观念:默认 100ms 时间片
实际:CFS 按可运行任务数动态推导
  sched_latency_ns(默认随 CPU 数增长,目标:一轮内让所有任务轮转一次)
  每个任务应得 ≈ latency × 权重占比
  下限 sched_min_granularity_ns(防止任务太多时单轮太碎)
  超过 sched_nr_latency 个任务后不再延长一轮,避免单轮总时长失控

所以任务越多,单个任务每轮分到的时间越少,但不会小于最小粒度。历史上把 sched_min_granularity_ns 调大能提高吞吐(少切换),代价是交互延迟——这个权衡在 EEVDF 里由 request 机制自动处理。

负载均衡:任务怎么跨 CPU 迁移

runqueue 是 per-CPU 的 → 必须周期性平衡
  调度域按拓扑分层:SMT(同核超线程)→ MC(同 socket)→ NUMA → 全机
  越往下层平衡频率越低(跨 NUMA 迁移要搬内存页,成本极高)

触发方式:
  ① 周期性:tick 里做 idle balance / 全局 balance(CONFIG_SCHED_HRTICK 关掉后靠 tick)
  ② 主动拉取:CPU 进入 idle 前,去相邻 busiest CPU 偷任务(newidle balance)
  ③ 唤醒选点:select_task_rq_fair —— 找空闲 CPU,考虑缓存热度与 NUMA 距离
     (wake_affine:刚睡眠的任务倾向于回到原来的 CPU,因为它的缓存还热)

NUMA 副作用:任务被拉过 NUMA 边界 → 内存还在远端节点 → 每次访问跨互联
  内核会做 AutoNUMA 页迁移(代价:额外缺中断),NUMA 敏感服务要留意

实践提示:“CPU 使用率整体不高但服务延迟高”,很常见的原因是任务全挤在几个核上,而其他核空闲。这时该看的是 per-CPU 分布(mpstat -P ALL),不是总使用率。

上下文切换的真实成本

进程切换        切页表基址(CR3)→ TLB 失效 → 后续大量 TLB miss + 缓存冷启动
线程切换(同进程)页表不变,只换寄存器/栈 → 便宜一档
内核线程/软中断  不离开内核态 → 更便宜

看指标:

vmstat 1        # cs 列:每秒切换次数
pidstat -w 1    # 按进程分自愿(cswch/s)和非自愿(nvcswch/s)

判读规则:

  • 非自愿切换高 → 任务被抢占,说明 CPU 真的不够(或权重被压制、cgroup 配额被限),要加 CPU 或降并发。
  • 自愿切换极高(数十万/s)→ 不是 CPU 问题,是任务频繁阻塞(锁、IO、过度同步),去查锁竞争和线程模型。
  • 切换次数本身没有"绝对超标值",要看它带来的 cache miss 是否体现在 IPC / 延迟上。

cgroup v2:容器里的调度真相

容器不是独立机器,它的 CPU 配额由 cgroup 决定,两个完全不同性质的参数:

cpu.weight(旧 v1 是 cpu.shares,0-10000,默认 100)
  性质:相对权重,只在**机器过载**时生效
  机器空闲时,低权重容器照样能跑满
  → 只影响"抢不到 CPU 时怎么分"

cpu.max "quota period"(如 "200000 100000" = 2 核)
  性质:绝对上限,任何时刻生效
  配额在每个周期开始时发放,用完 → 整个 cgroup 内所有线程一起 throttled,
  直到下一个周期(默认 100ms)才恢复

throttling 是延迟毛刺的头号来源:

周期 100ms,配额 2 核 = 200ms CPU 时间
应用有 8 个线程同时活跃 → 25ms 内把 200ms 配额烧光
剩下 75ms:整个进程被冻结,包括正在处理请求的线程
外部观测:P99 出现 100ms 量级的整数台阶

确认方法:

cat /sys/fs/cgroup/<容器>/cpu.stat
  nr_throttled / throttled_time   # 只要非零就说明配额成了瓶颈

应对:调大 cpu.max、减小线程池规模以匹配配额,或(不推荐)把 period 调小让毛刺变密变短。注意 Kubernetes 里 limits.cpu 直接映射到这个配额,requests.cpu 只映射权重——只设 requests 不设 limits 才不会被 throttle。

load average 到底在数什么

这是最常被误读的指标。它不是"CPU 使用率",而是平均活跃任务数:

load = 处于 R(可运行)+ D(不可中断睡眠)状态的任务数的指数滑动平均
       三个数:1 分钟 / 5 分钟 / 15 分钟

关键:D 状态也计入!
  → 磁盘 IO 阻塞、NFS 卡住、内核锁等待,都会把 load 抬高
  → 而这段时间 CPU 可能完全空闲

正确用法:

1. 除以核数:load 8 / 16 核 = 一半负载;load 8 / 4 核 = 排队中
2. 看三个数的趋势关系:
   1min > 5min > 15min  → 正在上升
   1min < 5min < 15min  → 正在回落
3. 与 CPU 使用率交叉判断:
   load 高 + CPU 高   → 计算密集过载
   load 高 + CPU 低   → D 状态堆积,去查 IO / 存储 / 内核阻塞
   load 低 + 延迟高   → 不是排队问题,看调度延迟、throttle、锁
4. 容器里 /proc/loadavg 是宿主机的(未做 lxcfs 隔离时),别拿来当容器指标

观测与调优入口

perf sched record -a -- sleep 5; perf sched latency     # 每个任务的等待时长分布(最直接)
cat /proc/schedstat                                   # runqueue 等待时间累计
cat /proc/sys/kernel/sched_latency_ns / ..._min_granularity_ns
cat /sys/kernel/debug/sched/debug                     # 每个 CPU 的调度器内部状态
taskset -c 0-3 ./app                                  # 绑核
chrt -f 50 ./app                                      # 设实时优先级

参数调整的边界:

目标 手段 风险
减少切换、提吞吐 提 min_granularity(旧内核)/ 降线程数 交互延迟上升
保延迟 SCHED_DEADLINE、isolcpus + 绑核 隔离核吞吐浪费,配置复杂
容器稳定 去掉 cpu limits 或放大配额 宿主过载时无保护
降低唤醒抖动 sched_wakeup_granularity_ns(旧) 迁移决策改变,可能加重不均

真正低延迟的服务(交易、音频)走的是"绑核 + 隔离 CPU + 关 irqbalance + 实时优先级"这套组合,而不是调 CFS 常数。

常见坑

  1. 拿 load average 当 CPU 使用率:D 状态会污染它,必须结合 CPU% 和 IO 等待一起看。
  2. nice 调 1 档期望见效:权重是指数关系,1 档几乎无感;要压就要 5 档以上。
  3. 给 Java/Go 进程设实时优先级:GC 线程、运行时后台线程会一起变实时,反过来饿死整个系统。
  4. 只看总 CPU 不看 per-CPU:负载全堆在 2 个核上,总量 20% 也照样排队。
  5. 容器设了 CPU limit 又不限制线程数:线程数远超配额 → 必然 throttle → P99 出现 100ms 台阶。
  6. 认为 cpu.weight 能保证拿到 CPU:它是相对权重,机器空闲才看得出差别,过载时才起作用。
  7. 用 top 的 CPU% 判断调度好坏:top 给的是采样窗口内的执行时间占比,不含等待时间,排队严重时使用率反而可能很高。
  8. 绑核后忽略中断与 NUMA:网卡中断默认在 CPU0,绑到 0 号核的关键线程会和中断抢;跨 NUMA 绑核导致内存访问在远端。

总结

问题 表面现象 机制根源 第一步查什么
吞吐低、切换多 cs 数十万/s 任务频繁阻塞或被抢 pidstat -w,先分自愿/非自愿
P99 整台阶 延迟有 100ms 倍数 cgroup CPU 配额 throttle cpu.stat 的 nr_throttled
load 高但 CPU 空闲 告警说负载高 D 状态堆积(IO/存储) vmstat 的 b 列 + IO 等待
个别任务莫名慢 等待时间抖动 唤醒选点/负载均衡迁移 perf sched latency
关键线程抢不到 CPU 实时性不达标 处于 fair 类被批任务压 提升到 RT/deadline 或隔离核

调度的核心不是"调哪个参数",而是先分清瓶颈属于哪一类:排队不够(缺 CPU)、切得太碎(开销)、被配额冻结(throttle)、还是根本没在等 CPU(阻塞在 IO)。四类问题的处置完全相反,判断错方向就会越调越糟。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。