本文地址:https://www.ebpf.top/post/schedblame-culprit-cpu-contention-ebpf

服务延迟突然升高,容器的 CPU 使用率却没到上限。看见同机还有一个忙碌容器,能不能直接把它迁走?

先别急着认定邻居有问题:容器可能没轮到 CPU,也可能被自己的配额挡住,还可能已经在运行,只是受共享缓存或内存带宽影响。三种情况,排查方法不同。

SchedBlame 关注其中的调度等待:目标容器已经能运行,却在某个 CPU 上等待时,其他容器在同一个 CPU 上运行了多久。它据此列出竞争者,而不是只报告等待总量。

这项工作来自滴滴出行的 Hao Li、Tonghao Zhang、Honglei Wang,见 2026 年 9 月 2 日提交的 SchedBlame arXiv 预印本。

截至 2026 年 11 月 8 日,论文仍以预印本公开,arXiv 版本记录只有 v1,未查到正式录用信息。

先把已有方案讲清楚,再来看它具体补了哪一块。

1. 同样是容器变慢:先分清等调度、受限额和运行中受干扰

容器最终还是由进程、线程执行任务。Linux 用控制组 (cgroup) 对这些任务进行资源管理,CPU 权重、配额和用量统计都与它有关。容器与 cgroup 的对应关系还取决于运行时和层级布局。

先设想一个具体场景:容器 A 的线程被唤醒,已经有活可做,但它所在 CPU 此刻正在运行容器 B 的线程。A 要先进入运行队列,等调度器选中它,才真正开始执行。这是等调度,不是 A 已经用满了自己的 CPU 配额。

如果 A 所属的 cgroup 已经耗尽本周期配额,情况就变了:即使还有线程想运行,也会暂时被限制调度,直到配额补充。这是节流,不能直接算到 B 头上。

从一次请求的视角看,“没在推进”至少有几种原因。

所处状态 发生了什么 优先查什么
能运行,暂未获 CPU 任务处于可运行状态,仍在等调度 运行队列等待、同 CPU 的运行者
配额用完,暂时不能调度 当前配额周期内被 CPU 控制器节流 自身及父级 cgroup 的配额
已经在 CPU 上运行,执行效率下降 可能有共享缓存、内存带宽等干扰 硬件性能计数器、访问模式

还有等磁盘、等网络、等锁等情况,它们也会拉长请求时间,不能一律算成等 CPU。

这里的区别不是咬文嚼字。如果问题是父级配额太紧,把某个邻居迁走未必有用;如果是共享缓存干扰,调度等待可能并不突出。

SchedBlame 主要针对第一行。它不是覆盖所有性能干扰的通用根因工具。

2. 先知道有没有在等:PSI 和运行队列延迟仍是排查入口

压力停顿信息 PSI 可以告诉我们,任务有多少时间因为资源压力而无法推进。以 CPU 压力中的 some 为例,它统计至少有部分任务因 CPU 资源而停顿的时间占比。

另一类常用信号是运行队列延迟:任务已经可以执行,从开始等调度到真正获得 CPU,经历了多久。runqlat 把这样的等待汇总为分布,便于看典型水平以及少量很长的等待。

两者都很有用,但回答的问题不同于竞争者归因:

  • PSI 帮助判断 CPU 压力是否持续存在。
  • 运行队列延迟帮助判断每次等调度有多严重。
  • 配额节流统计帮助判断是不是自身资源限制在起作用。

它们通常不会直接给出“这个目标等待时,各个邻居分别运行了多久”。容器 CPU 用量高,也只能说明它比较忙,不能单凭这一点证明它导致了另一个容器变慢。

当然,可以收集详细调度事件,事后重建运行时间线。难点在于繁忙主机的事件量、用户态处理成本和数据保留成本。短时诊断与长期常开,面对的工程约束并不一样,不能简单说全量跟踪没有价值。

3. 已有方案各有所长:Netflix 看调度事件,CPI² 和 PANDA 看性能干扰

3.1 Netflix:把等待延迟和切换关联信息放在一起看

Netflix 在 Noisy Neighbor Detection with eBPF 中介绍了 Titus 多租户平台的持续观测方案。

它先记录任务被唤醒、变成可运行状态的时间;等任务真正被选中执行,再计算两者之差,得到运行队列延迟。实现借助的是调度器的唤醒和切换事件,无须修改业务代码。

但只看延迟还不够。一个容器耗尽自己的 CPU 配额,也可能出现等待升高。因此 Netflix 同时记录切换前后任务所属的 cgroup,向监控系统输出 runq.latency 和 sched.switch.out,并关联同容器、其他容器或系统服务等类别。

这让排障从“它等了很久”,前进一步到“延迟上涨时,是否同时出现了与外部容器或系统服务关联的切换计数尖峰”。这是一种已有生产经验的方案,不是等待被新论文替代的落后工具。

它和 SchedBlame 的关键区别,是计量单位。 一次切换关联信息,不等于目标整个等待期间的所有运行者;事件次数也不能直接换算成对方消耗的 CPU 时间。

例如,目标已经在排队,其他几个组轮流运行,目标却一直没被选中。这段等待不要求每个竞争者都先把目标从 CPU 上抢占下来。SchedBlame 论文据此提出:还应记录等待期间的运行时长,而不只是某次抢占或切换。

3.2 CPI²:已经拿到 CPU,为什么仍然跑得慢?

Google 的 CPI² 面对的是另一类重要问题:共享缓存、内存总线等资源缺少充分隔离,即使应用正在运行,也可能被其他任务拖慢。

CPI 是每条指令平均消耗的周期数。CPI² 通过硬件性能计数器收集这类数据,从同一作业的多个任务中学习正常表现,找出异常,再通过在线相关性分析筛选可能的干扰者;系统还可以选择对疑似干扰者限速。

通俗地说,它不只问“有没有轮到我运行”,还问“轮到以后,每执行一条指令是不是变费劲了”。

因此,CPI² 与 SchedBlame 不是同一道题的两种等价答案。前者能触及运行期间的共享硬件干扰,后者着重解释等待调度时的 CPU 占用关系。不能因为一个使用统计方法,就认定它应当被调度跟踪取代。

3.3 PANDA:用全局历史数据改善干扰者识别

PANDA 也使用 CPI,但着重处理生产采样噪声和多个受影响任务并存的问题。

只看一台机器上的短期数据,可能出现这种情况:同机几个任务一起变慢,分别做相关性分析,却各自指向不同的疑似干扰者。PANDA 把跨机器、跨时间的历史信息汇总到作业层面,并引入机器级 CPI 指标,试图让识别更稳定。

它的论文报告了多受影响任务场景下的一致性改进。早期局部相关性方法的局限,正是它试图改善的问题,不能原样套到 PANDA 上。

另有 Volpert 等发表于 ICPE 2025 的研究,其摘要介绍了另一条路线:使用调度延迟与抢占频率在线识别嘈杂邻居,无需预先建立负载画像或做离线分析。它也说明,调度事件观测本身已有前作;讨论 SchedBlame 的贡献,应当具体到它增加的时间记账关系,而不是笼统地说过去只能看相关性。

把这些方案摆在一起,可以按“它主要回答哪一个问题”来选,而不是排一张通用优劣榜:

方案 主要看什么 不能单独回答什么
PSI CPU 压力造成的停顿占比 目标等待时各竞争者分别运行多久
runqlat 每次排队等待的延迟分布 等待期间的竞争者时间分摊
Netflix 实践 等待延迟与切换关联的 cgroup、类别和计数 整个等待区间内各竞争者的累计运行时长
CPI² 运行效率异常与共享硬件干扰者线索 同 CPU 调度等待的逐竞争者时间账
PANDA 利用全局历史和机器级 CPI 改善干扰者识别 同 CPU 调度等待的逐竞争者时间账
ICPE 2025 研究 根据调度延迟与抢占频率识别干扰 仅凭公开摘要,不能确认完整的竞争者时间分摊能力

表中 Netflix、CPI²、PANDA 的区别按公开机制整理;ICPE 一项只依据可访问摘要。SchedBlame 选择补上的是同 CPU 等待期间的竞争者时间账,不是替代以上所有观测手段。

4. 一笔 10 毫秒的等待账:B 跑了 6 毫秒,C 跑了 4 毫秒

先看一个假设例子,不是论文实验或工具实测。只看 CPU 7:A 在第 0 毫秒就已可运行,整个等待期间没有被配额节流;B、C 都是其他容器。为方便理解,假设记录完整、状态变化恰好落在运行片边界,不涉及采样和快照误差。

时间段 CPU 7 上谁在运行 A 的状态
0~6 毫秒 B,运行 6 毫秒 可运行,在等 CPU
6~10 毫秒 C,运行 4 毫秒 可运行,继续等 CPU
10~15 毫秒 A,运行 5 毫秒 已得到 CPU

A 等待的 10 毫秒,就按运行者拆成两笔:B 记 6 毫秒,C 记 4 毫秒。竞争者排行因此是 B 在前、C 在后;这两笔分别占该段外部等待账的 60% 和 40%。B、C 无须先把 A 抢占下来,只要在 A 等待时占用了同一个 CPU,就可能被记入。

图1 同 CPU 等待时间线与两种百分比,假设示例非实测

这里有两个不能混用的百分比:60% 是 B 在外部等待账中的份额,不是 A 的外部争抢比例。 按现行工具文档,A 的运行时间为 5 毫秒,外部争抢时间为 10 毫秒,运行与外部争抢合计为 15 毫秒,其中外部争抢占约 66.7%。两者分母不同。

同一个 CPU、时间发生重叠、目标确实可运行但未运行,三个条件缺一不可。 B 在另一个 CPU 上忙,不会仅凭“忙”被记到这笔调度等待账里。

还要提前说明一个口径:运行、内部争抢、外部争抢和节流,不能想成互不重叠的四段时间。现行工具把内部争抢记为目标运行时、同组另有任务排队的时间,它包含在运行时间中。

再给运行阶段补一个假设:10~13 毫秒由 A 的线程 A1 运行,线程 A2 在同 CPU 排队;到第 13 毫秒,A1 阻塞,换 A2 运行至第 15 毫秒。按这两个运行片记账,A 的总运行时间仍是 5 毫秒,内部争抢时间为前一个运行片的 3 毫秒,内部比例为 60%;不能再把这 3 毫秒加到运行时间上。外部比例仍按前面的运行与外部争抢计算,节流不进入现行文档定义的这两个比例。论文使用了另一套分母,第 6 节再对照。

最后,这笔账不等于请求延迟损失。它没有说明等待任务是否处于某个请求的关键路径,也没有重演迁走 B 后调度器会怎样选任务;请求还可能包含 I/O、锁等待和并行执行。因此,B 被记了 6 毫秒,不代表迁走 B 就一定让请求快 6 毫秒,更不代表 B 违规使用资源。

5. 为了长期常开:内核记等待集合,用户态展开竞争关系

5.1 用位图回答谁在等,而不是逐对维护计数

假如每个运行者都要在内核里逐一查找所有等待目标,再更新每一对的计数,调度器的频繁执行路径就会做很多工作。

SchedBlame 的办法,是为每个 CPU 保存一个等待位图。位图可以理解为一排开关:每个被测目标占一位,亮起表示该目标在这个 CPU 上至少有一个任务正在等。

这不是每个线程一位,也不是等待人数。一个目标在同 CPU 上排了多个任务,这一位仍然只是 1。因此它衡量的是目标在该 CPU 上的运行机会,而不是所有排队任务等待时间的简单相加。

等待状态主要来自内核已有的可运行实体计数 h_nr_running。如果 CPU 正在运行这个组,就要扣除正在运行的那个实体,才能判断是否还有任务等待;如果运行的是别的组,这个目标的可运行实体都在等。

一次运行片结束时,记录携带运行者身份、运行时长以及等待位图。用户态再把这段时间展开,累计给快照中的各个等待目标。内核不保存每一对“运行者—等待者”的计数矩阵,把关系展开留到用户态处理。

论文默认配置的一条运行片记录为 16 字节,可表示 84 个被测目标的等待状态。这并不表示整套工具只占 16 字节,也不表示能同时精确跟踪无限多容器。

5.2 抽样可以少看记录,但不能少维护状态

运行片很多,可以只保留其中一部分,以减少传输和用户态处理成本。但调度事件发生时,对等待位图的维护仍然必须执行。

用户态再按保留概率对时长进行缩放。这里的无偏估计,只针对设计好的随机抽样:它不等于位图测量本身没有误差,也不能补偿缓冲区拥塞造成的丢数据。

每条运行片带着自己的等待快照,因此丢一条传输记录,不会让用户态后续一直沿用错误的等待状态。这不代表丢失没有损失:本次没送达的 CPU 时间仍会少算,后续记录也仍受各自的测量近似影响。

5.3 不改内核,仍然有版本适配成本

为了支持论文中的 4.18 内核,传输使用 perf event array,并把多条运行片攒成批次,避免每次切换都单独上送。

目标集合变化时,工具通过更新一代配置编号 (epoch),使旧的分类缓存和等待位图失效,再由后续事件重建。这避免了为了换一组监控目标而暂停所有钩子,但会带来短暂少报窗口。

这些设计的价值,是把内核中的高频工作限制在有界范围内。不过,读内核内部计数、依赖钩子触发顺序,仍需要逐版本验证;“不打内核补丁”不等于“不需要维护适配”。

6. 代码已公开,但指标定义与论文不同

HUATUO 公开仓库 已提供独立的 SchedBlame 命令入口,官方有独立运行文档和分析文档。读者已有公开的构建、输出和分析入口。

先按现行分析文档理解输出。令 (R) 表示目标运行时间,(E) 表示估算的外部争抢时间,(I) 表示运行期间存在内部争抢的时间。外部比例为:

$$ r_{\mathrm{project}}=\frac{E}{R+E} $$

内部比例则是 (I/R)。(I) 与 (R) 重叠,节流时间不进入这两个比例。(R+E=0) 时外部比例不可用,(R=0) 时内部比例不可用;没有数据与有效的零值也要区分。

再看论文 v1。论文把运行、内部争抢、外部争抢和节流相加,定义一个需求量,外部比例为:

$$ r_{\mathrm{paper}}=\frac{E}{R+I+E+T} $$

其中 (T) 是论文记入的节流时间。这是论文自己的定义,不是现行工具文档的公式。把论文里的四项相加,不能理解为现行界面上四段互斥的时间。即便名称相同,也要先核对时间定义、是否重叠以及分母,不能直接拿百分比比较严重程度。第 4 节的 66.7% 明确按现行文档计算,不代表论文比例。

图2 现行工具与论文 v1 的指标口径对照

内核范围也要分开看:论文报告在未修改的 4.18、5.10 和 cgroup v1 上测试;独立工具文档声明支持 Linux 4.18 到 6.5,并列出调度分组和带宽控制等配置前提。后者是项目声明。

论文报告的开销如下,采用不采样配置:

项目 论文报告 应如何理解
测试环境 一台 96 核 Intel Xeon 主机,84 个生产容器 单一环境下的初步开销结果
Redis 吞吐 下降约 1% 挂载与不挂载钩子的比较
用户态进程 约占单核的 6% 折算整机约 0.06%,不是整机 6%

论文还称未观察到 Redis p99 延迟的显著变化,但没有给出完整压测参数。评估覆盖到哪里、还缺哪些验证,集中放在下一节说明。

7. 把排行用于排障:先核对测量边界,再验证业务影响

论文的初步评估衡量了开销,尚未给出完整的归因准确度、告警精确率和召回率,也未完成与 Netflix 式方案等方法的实验对照。因此,目前能解释机制差异,不能宣称检测效果已全面更好。公开代码也不自动补齐这些证据。

下面是论文 v1 披露的主要使用边界。本文未执行工具测试,也未逐项审计当前代码是否已经修复;实际部署应按具体版本核对,不能把论文风险清单直接当成最新实现的缺陷清单。

首先看配额设在哪一层。 论文版本只跟踪目标自身的直接节流。如果父 cgroup 的配额耗尽,目标因此无法运行,这段时间可能误记成外部争抢。作者将其列为最重要的误归因之一。层级配额场景中,应先确认部署版本如何处理祖先节流,再据此判断邻居。

其次看快照近似。 论文版本使用运行片结束时的等待状态,给整个运行片记账。目标如果中途才开始等待,可能多记;中途停止等待,可能少记。这里存在双向误差,并不意味着任何真实负载下都会自动相互抵消。随机抽样的无偏性,不能替这种边界近似作保证。

再看覆盖范围与数据完整性。 论文默认最多同时测 84 个目标,竞争者身份字段还有限定范围;目标也不是自动包含全部后代 cgroup。任务迁移、身份复用、目标集合调整和传输丢失,都可能影响当期结果。缓冲区越忙时越容易丢数据,按随机采样概率放大,并不能把这些数据补回来。

告警规则也不是根因结论。论文把每秒外部比例与历史 P99 比较,但 v1 的阈值没有截断到比例上限,某些高基线配置可能抑制告警。项目文档另外要求足够的有效历史样本和正的基线。因此,没收到事件并不等于没有争抢。

最后别忘了最初的问题边界:论文方案针对 CFS 调度队列竞争,不覆盖显著的实时、deadline 调度负载,也不解释共享缓存、内存带宽或 SMT 兄弟线程上的干扰。遇到这些情况,硬件计数器与其他观测手段仍不可替代。

如果准备在自己的环境验证,我们更建议先用可控的同 CPU 竞争负载建立已知条件,再分别测试父级配额、多个目标同时等待以及高负载传输丢失;确认输出符合预期之后,再把排行作为迁移、限额等动作的依据。

8. 总结

从现有指标到 SchedBlame,真正增加的不是又一个“CPU 很忙”的告警,而是更具体的关系:目标在同 CPU 等调度时,哪些 cgroup 正在运行,各运行了多久。

PSI 和运行队列延迟适合发现等待;Netflix 的实践把等待与切换关联信息结合起来;CPI²、PANDA 从性能计数器和统计分析解释另一类共享资源干扰。它们面对的问题部分交叉,但没有谁能单独解释所有容器变慢。

实际排查时,可以先看配额与等待信号,再用竞争者时间账缩小范围,最后通过受控验证决定是否迁移或调整限额。SchedBlame 增加的是这一步中的可解释线索;位图、运行片快照和状态维护与采样分离,则说明了如何以有界成本持续收集这些线索。

延伸阅读

若还想顺着读,可看这几篇:

  1. 《当 BPF 邂逅 CPU 调度器》
  2. 《BPF 赋能调度器:万字详解 sched_ext 实现机制与工作流程【建议收藏】》
  3. 《偶发夯住 15 秒:LinkedIn 用 eBPF off-CPU 轻松拿捏》