题干与适用场景
一台 16 vCPU、Linux 6.12、使用 cgroup v2 的服务器,同时运行延迟敏感 API 与离线压缩任务。离线任务启动后,API p99 从 45ms 上升到 600ms。监控显示整机 CPU 使用率约 78%,API 自身累计的 CPU 时间不多;API 和离线任务的相关线程都采用 SCHED_OTHER。
请解释现代 Linux 公平调度器如何在可运行线程之间做选择,并给出一套可以验证的诊断流程,区分以下原因:
- API 线程已经 runnable,却长期排不到 CPU;
- API 所在 cgroup 被
cpu.max配额节流; - affinity 或 cpuset 把任务限制在少数繁忙 CPU 上;
- API 线程实际在等待互斥锁、I/O 或其他事件;
- 更高优先级的实时线程让普通线程得不到运行机会。
这道题适合 SRE、基础设施、系统软件、性能工程和需要理解 Linux 运行时的后端岗位。16、6.12、45ms、600ms 和 78% 都是虚构的练习条件,不代表通用容量或告警阈值。
面试官考察点
第一层是状态边界。调度器只能从 runnable 线程中选择下一位执行者。线程睡在互斥锁、网络、磁盘或定时器上时,它没有在 CPU 运行队列里等待;这段时间不能全部记作调度延迟。候选人需要先回答“线程是否已经可运行”。
第二层是公平调度模型。历史 CFS 解释用虚拟运行时间 vruntime 近似“理想多任务 CPU”,优先选择 vruntime 较小的实体。当前 EEVDF 模型继续追踪公平欠账 lag,只在符合资格的实体中选择虚拟截止时间最早者。nice 或 cgroup 权重影响长期相对份额;时间片与虚拟截止时间影响任务多快获得下一次运行机会。二者都没有为单次请求承诺固定 p99。
第三层是层级与局部约束。整机仍有 22% idle,无法排除目标 cgroup 已用完硬配额,也无法排除线程被固定在两个繁忙 CPU。cpu.weight 是有竞争时活动兄弟 cgroup 之间的相对权重,cpu.max 是每个周期内的硬带宽上限。任务组与 cpuset 会让主机平均值掩盖局部饥饿。
第四层是直接证据。/proc/12345/schedstat 的运行队列等待时间、perf sched timehist 的 runnable-to-running 调度延迟、cgroup 的 cpu.stat 与 cpu.pressure,比单独看总 CPU 或上下文切换次数更接近问题本身。强回答还会说明采样权限、开销与时间窗口。
最后是处置边界。把 API 改成 SCHED_FIFO 可能让它抢占普通任务,也可能让无界循环或锁持有者饿死整机。修复必须跟随证据:配额错误就调整配额,权重不足就调整兄弟权重,亲和性错误就修复 CPU 集合,线程没有 runnable 就调查锁或 I/O。
回答前需要澄清的问题
- p99 从哪里计时? 要区分入口排队、应用执行、下游等待与客户端网络时间,并把主机采样对齐到同一时间窗口。
- 目标线程何时 runnable? 需要线程状态、off-CPU 原因或调用栈。低 CPU 时间可能来自排不到 CPU,也可能来自长期睡眠。
- API 与离线任务处于什么 cgroup 层级? 比较各自的
cpu.weight、cpu.max、父级限制和cpu.stat;父 cgroup 的限制也会约束子组。 - 允许在哪些 CPU 上运行? 核对线程 affinity、
cpuset.cpus.effective、每 CPU 利用率与 NUMA 布局。16 vCPU 只是主机容量,不等于线程有权使用 16 个 CPU。 - 是否存在实时调度实体? 查看
SCHEDFIFO、SCHEDRR的优先级、CPU 亲和性与运行时。题干仅说明相关业务线程为SCHED_OTHER,没有排除同机其他实时线程。 - 离线任务是否改变锁、内存或 I/O 路径? 压缩任务可能造成 CPU 竞争,也可能加剧内存回收、文件 I/O 或共享线程池排队。
- 能否进行短时跟踪?
perf sched通常需要额外权限,并会产生记录开销。先用低开销计数器确认方向,再在可控窗口内采样 15 秒。
30 秒回答框架
“我先确认 API 线程在慢请求期间是否已经 runnable。睡在锁或 I/O 上的时间属于 off-CPU 等待;只有 runnable 到真正 running 的间隔才是这里要验证的调度延迟。
Linux 6.12 的公平类调度应按 EEVDF 理解:调度器维护实体相对理想公平份额的 lag,在符合资格的实体中选择虚拟截止时间最早者。CFS 的最小 vruntime 仍能解释历史与公平直觉,但不足以完整描述当前选择规则。nice 与 cpu.weight 调整相对份额,cpu.max 会直接节流,affinity/cpuset 决定任务可以去哪些 CPU。
我会先看每 CPU 利用率、线程状态和 cgroup 配置,再比较 /proc/12345/schedstat 的 runqueue wait 增量、cpu.stat 的节流增量和 cpu.pressure。方向仍指向调度器时,短时采集 perf sched,直接观察 runnable-to-running 延迟。修复后在同一负载下同时验证 API p99、调度延迟分布、节流、CPU pressure 与离线吞吐,避免用整机 78% CPU 推断根因。”
分步骤深入解答
第一步:先划清 runnable 与 sleeping
Linux 调度器在某个 CPU 需要选择下一个任务时,只考虑该 CPU 或可迁移到该 CPU 的 runnable 实体。普通线程的调度策略优先级为 0;实时 SCHEDFIFO/SCHEDRR 线程使用更高的静态优先级范围,并能抢占普通线程。
API CPU 时间少存在两类完全不同的解释:
- 线程已经 runnable,因运行队列竞争、配额或 CPU 集合限制而迟迟没有 running;
- 线程在 futex、socket、磁盘、定时器或应用队列上 sleeping,事件到来后才重新 runnable。
先用线程视图观察状态与调度策略:
ps -eLo state,cls,rtprio,pri,ni,psr,pid,tid,comm --sort=-rtprio,-pri
pidstat -t -w -u -p "$pid" 1 10ps 是瞬时快照,不能靠一次 R 或 S 下结论。pidstat 的上下文切换和 CPU 数据可帮助选择目标线程,但上下文切换多也不等于调度延迟高。若线程主要 sleeping,继续用应用 trace、锁剖析、off-CPU stack、I/O 与下游延迟定位唤醒条件。
第二步:用 CFS 建立直觉,再更新到 EEVDF
CFS 的经典解释把每个公平类实体的实际运行时间按权重缩放到 vruntime。权重较高的任务,vruntime 增长相对较慢;选择较小 vruntime 的实体,能让长期服务份额靠近配置权重。这个模型仍有教学价值,也能解释 nice 改变公平份额的方向。
Linux 内核文档说明,公平类调度从 6.6 开始向 EEVDF 过渡。可以用两个概念回答:
lag表示实体相对于理想公平服务已经多拿或少拿了多少;lag >= 0的实体符合资格;- 在符合资格的实体中,调度器选择虚拟截止时间最早者。较短的请求时间片能获得更早截止时间,改善对延迟敏感任务的响应机会。
EEVDF 同时处理公平与延迟取舍,但没有从负载权重推导出固定毫秒级 SLA。任务能否快速执行还受可用 CPU、任务数量、权重层级、quota、唤醒时机和高优先级调度类影响。面试回答可以先讲 CFS 的 vruntime 直觉,再明确当前题目的 Linux 6.12 要用 EEVDF 选择规则收尾。
第三步:分清 nice、权重、硬配额和 CPU 集合
对 SCHED_OTHER 线程,nice 的合法范围通常为 -20 到 19;Linux 按线程维护 nice。它改变公平类中的相对权重,并不预留 CPU,也不会解除 cgroup 上限。启用任务组调度时,不同 cgroup 的实体先在组层级竞争;只给组内某个线程 renice,可能无法显著改变两个 cgroup 之间的份额。
cgroup v2 中三个控制面回答不同问题:
cpu.weight默认为 100,范围 1 到 10000;它按活动兄弟之间的相对比例分配有竞争的 CPU 周期;cpu.max形如$MAX $PERIOD,默认max 100000;数值上限会让整个 cgroup 在周期预算耗尽后被节流;cpuset.cpus.effective表示层级与系统状态共同决定的实际可用 CPU 集合。
读取主机、线程和目标 cgroup 的边界:
mpstat -P ALL 1 10
taskset -pc "$pid"
cat /sys/fs/cgroup/api/cpuset.cpus.effective
cat /sys/fs/cgroup/api/cpu.weight
cat /sys/fs/cgroup/api/cpu.max
cat /sys/fs/cgroup/api/cpu.stat
cat /sys/fs/cgroup/api/cpu.pressure若 cpu.stat 的 nrthrottled 与 throttledusec 在慢请求窗口同步增长,说明 cgroup 确实触发了带宽上限。即使整机还有空闲 CPU,硬配额仍可以阻止该组继续执行;这是 cgroup 预算与主机容量作用范围不同的直接结果。若配额未触发,但 cpuset.cpus.effective 只有 2-3,而 CPU 2、3 已满,主机 78% 的平均值同样会误导。
第四步:直接测量 runnable 到 running 的等待
选定代表性慢线程后,连续读取其调度统计:
cat /proc/"$tid"/schedstat
sleep 5
cat /proc/"$tid"/schedstat三个字段依次是该线程累计在 CPU 上运行的纳秒数、累计在运行队列等待的纳秒数,以及获得 CPU 的时间片次数。用前后差值而非累计总数判断当前窗口。若第二个字段快速增加而线程运行时间很少,支持“已经 runnable 但没有及时 running”;它仍需要与配额、亲和性和高优先级线程一起解释。
需要分布与时间线时,做短时系统级跟踪:
sudo perf sched record -a -- sleep 15
sudo perf sched timehist --state --summaryperf sched timehist 可以显示 wait time、从 runnable 到 running 的 scheduling delay 与实际 runtime,并把唤醒、迁移和 CPU 串在一条时间线上。生产环境先确认内核支持、权限、磁盘空间与可接受开销,限定采样窗口,结束后删除不再需要的 trace 数据。不要因为一次 15 秒采样没捕获尾部事件就宣布没有调度问题;应在可复现负载下多次比较变更前后分布。
第五步:用互斥证据定位五类原因
可以按下表收敛:
| 证据组合 | 结论方向 | 下一步 | |---|---|---| | runqueue wait 与 perf sched delay 高,CPU pressure 高,未节流 | 公平类运行队列竞争 | 看每 CPU 队列、权重、批任务并发与迁移 | | nrthrottled、throttledusec 随 p99 增长 | cgroup 硬配额耗尽 | 校验父子 cpu.max 与容量预算 | | 少数 CPU 满载,其他 CPU 空闲,effective cpuset 很窄 | affinity/cpuset 局部热点 | 检查绑核意图、NUMA 与可迁移范围 | | 线程大部分时间 sleeping,runqueue wait 低 | 锁、I/O、定时器或应用队列等待 | 查 futex/off-CPU 栈、I/O 与下游 trace | | 同一 CPU 上有长时间运行的高优先级 FIFO/RR 线程 | 实时调度饥饿 | 审核实时优先级、runtime、锁和亲和性 |
cpu.pressure 衡量可运行任务因为 CPU 竞争无法推进的时间。它很适合确认“CPU 争用影响了工作负载”,但不会替你区分相对权重不足、配额节流或绑核错误。cpu.stat、affinity 与调度轨迹负责完成这一步。
实时线程是另一条优先级路径。SCHEDFIFO 线程会持续运行,直到阻塞、被更高优先级实时线程抢占或主动 yield;SCHEDRR 才在相同优先级之间加入时间片。普通公平类线程的权重再高,也不能越过持续 runnable 的高优先级实时线程。
第六步:按根因修复,并用业务与调度指标共同验收
修复动作应与已确认的机制一一对应:
- 配额错误:在完成容量与邻居影响评估后提高或移除错误的
cpu.max; - 相对份额不足:提高 API cgroup 权重,降低离线 cgroup 权重,或降低离线任务 nice;
- CPU 集合错误:扩大或重新平衡 cpuset/affinity,同时核对 NUMA 和缓存局部性;
- 公平类竞争:限制离线并发,拆分批次,必要时让离线任务使用
SCHEDBATCH或SCHEDIDLE; - sleeping 等待:修复锁竞争、线程池、I/O、内存压力或下游服务;调度参数通常没有收益;
- 实时饥饿:移除不必要的实时策略,限定运行时间并审查优先级反转和锁依赖。
在相同请求率与离线负载下做前后对照。至少验证 API p50/p95/p99 与错误率、每线程调度延迟分布、schedstat runqueue wait 增量、cgroup throttling、CPU pressure、每 CPU 利用率和离线吞吐。优化 API p99 时不能让离线任务永久饥饿,也不能把压力转移到下游或其他租户。
高质量示范回答
“整机 CPU 78% 不能排除调度问题。Linux 调度的是线程,约束可能位于线程、CPU 或 cgroup 层级。我先确认慢请求对应线程是否 runnable;若它在 futex 或 I/O 上睡眠,低 CPU 时间主要是等待唤醒,调度器没有可选它。
题目是 Linux 6.12。CFS 的 vruntime 能解释公平直觉,但当前选择应按 EEVDF 表述:调度器追踪任务相对理想份额的 lag,只让 lag >= 0 的实体参与资格竞争,再选择虚拟截止时间最早者。nice 和 cpu.weight 调整长期相对份额,cpu.max 设置周期硬上限,affinity/cpuset 则限制可运行 CPU。这些机制不会自动保证 45ms p99。
我先执行 mpstat -P ALL,再读取 API cgroup 的 cpuset.cpus.effective、cpu.weight、cpu.max、cpu.stat 和 cpu.pressure。如果慢窗口中 nrthrottled、throttledusec 持续增加,配额节流已有直接证据;主机仍有 idle 不影响这个结论。如果只允许 CPU 2、3,而这两个 CPU 已满,我会优先修复局部 CPU 集合。
若没有节流或绑核异常,我会对代表性 TID 读取两次 /proc/12345/schedstat,看运行队列等待增量。能够复现时短时运行 perf sched record 和 timehist,直接检查 runnable-to-running 调度延迟、唤醒者与 CPU。runqueue wait 很低且线程长期 sleeping 时,我会转查锁、I/O、应用队列和下游 trace。
假设最终证据是 API cgroup 配置为 20000 100000,节流与 p99 同步增长,我会先按测得容量修正预算,并限制离线并发;不会把 API 直接改成 SCHED_FIFO。复测使用同样流量和批任务,要求 API p99、调度延迟、throttling 与 CPU pressure 一起下降,同时确认离线吞吐仍可接受、没有任务饥饿。”
常见错误
- 把 78% 整机 CPU 当作“没有 CPU 问题” → 平均值会掩盖 cgroup 配额与局部 CPU 集合 → 同时查看每 CPU、cgroup 与线程。
- 把所有 off-CPU 时间叫调度延迟 → sleeping 线程尚未进入 runnable 队列 → 先用状态、栈与 trace 确认唤醒点。
- 只背“CFS 选择最小 vruntime” → Linux 6.6 起的公平类调度已向 EEVDF 过渡 → 说明 eligible lag 与最早虚拟截止时间。
- 认为
cpu.weight=200就保留两颗 CPU → 权重只描述有竞争时活动兄弟之间的相对份额 → 容量保证需要单独设计,硬上限看cpu.max。 - 认为 nice 能跨所有容器直接排序 → 任务组会先按 cgroup 层级竞争 → 先看层级权重,再决定是否调整线程 nice。
- 看到上下文切换多就断言调度器慢 → 切换可能来自正常 I/O 或高并发 → 测量
schedstatrunqueue wait 与perf scheddelay。 - 把 API 改成
SCHED_FIFO作为快速修复 → 高优先级实时线程可能饿死普通任务并放大锁风险 → 仅在真实截止时间、受控 runtime 和完整分析下使用实时策略。 - 只提高 API 权重,不看 quota → cgroup 到达
cpu.max后仍会被节流 → 用cpu.stat证明节流并修正限制。 - 只看 API p99 → 修复可能以批任务饥饿或其他租户退化为代价 → 同时验收公平性、吞吐、压力与错误率。
追问及应对
追问一:为什么主机还有 idle CPU,cgroup 仍会被节流?
cpu.max 限制的是该 cgroup 在一个周期内可消耗的 CPU 带宽。预算耗尽后,该组要等下一周期补充,其他不受限的 CPU 可以保持 idle。检查 cpu.max,并在相同窗口观察 cpu.stat 的 nrthrottled 与 throttledusec 增量;这比主机平均利用率更能回答是否触发硬上限。
追问二:为什么给 API 线程 renice 后可能没有明显改善?
Linux nice 按线程生效,但启用组调度后,不同 cgroup 先作为调度实体按组权重竞争。renice 主要改变线程在本组内部的份额,无法解除父级 cpu.max,也不一定改变 API 组与离线组之间的比例。先绘制 cgroup 层级和权重,再选择调整线程 nice 或组 cpu.weight。
追问三:把 API 改成 SCHED_FIFO 能否修复 p99?
它可能缩短目标线程等待,也会引入更高风险。持续 runnable 的 FIFO 线程可以压制所有普通公平类线程;若它忙循环、持锁或依赖被其饿死的普通线程,系统可能失去进展。只有工作确有实时截止时间、运行时间有严格上界、优先级反转已处理且具备回退保护时,才评估实时调度策略。
追问四:EEVDF 是否保证请求在一个时间片内得到 CPU?
不保证。虚拟截止时间用于在符合资格的调度实体之间排序,时间片表达延迟偏好;可用 CPU、实体数量、权重、quota、affinity 与更高调度类仍会影响实际等待。它提供调度机制上的公平与延迟权衡,不等同于业务 p99 SLA。
追问五:怎样区分 mutex 等待与运行队列等待?
mutex 等待期间线程通常 sleeping,等待锁释放后才变为 runnable;运行队列等待发生在 runnable 之后。把应用或 eBPF off-CPU 栈、futex/锁指标与 /proc/12345/schedstat、perf sched timehist 对齐:大量 futex 睡眠且 runqueue wait 很低指向锁,大量 runnable-to-running delay 指向调度或资源控制。
追问六:什么时候固定 CPU 反而有帮助?
合理的隔离可以减少迁移、缓存抖动和噪声邻居,例如为经过容量验证的延迟敏感线程保留 CPU。前提是 CPU 集合足够、有中断与后台任务治理,并持续监控单核队列。错误绑核会在主机有空闲容量时制造局部拥堵,所以必须用每 CPU 利用率和调度延迟验证效果。