☰
Linux调度器性能分析实战:perf sched延迟排查与优化指南
2026/10/15 5:05:11 网站建设 项目流程

1. 调度器性能分析为什么值得单独拎出来讲

做Linux性能优化的人,迟早会碰到一类问题:系统CPU占用不高、内存也够、IO看起来也不忙,但应用的响应就是一阵一阵地卡,延迟毛刺特别明显。这种时候你去看top,大概率看不出什么名堂,因为问题根本不在资源总量上,而在任务在CPU之间的迁移、等待和唤醒路径上。perf sched就是专门用来啃这块硬骨头的工具。

perf sched是perf工具集里针对调度器子系统的分析命令,它能记录一段时间内所有的调度事件——任务切换、唤醒、迁移、阻塞——然后把这些原始事件重放成可读的统计视图。核心子命令包括perf sched record、perf sched latency、perf sched map、perf sched timehist、perf sched replay等。其中latency和timehist是日常排查用得最多的两个,前者给你每个任务的调度延迟汇总,后者给你逐事件的调度时间线。

这篇文章适合谁看?如果你是在做服务器性能调优、嵌入式实时性分析、或者单纯想搞清楚"我的进程为什么被踢来踢去",那这篇内容基本能覆盖你80%的日常需求。如果你连perf都没装过,也没关系,我会从最基础的环境准备讲起,把每个参数为什么这么设、每个数字代表什么含义都掰开说。全文基于我这些年在一线排查调度问题攒下来的经验,很多细节是官方文档里不会写的,比如-w的宽度到底怎么选、timehist里那个wait time和sch delay到底差在哪、为什么有时候record出来的数据全是乱的。

先说一个最容易被忽略的前提:perf sched分析的是调度器视角的时间,不是应用视角的时间。这两者之间的差距,恰恰就是你要找的答案。举个例子,一个线程从被唤醒到真正上CPU跑,中间可能经历了唤醒延迟、迁移开销、被更高优先级任务抢占等好几个阶段,perf sched能把这些阶段拆开给你看。这就是它比top、vmstat这类粗粒度工具强的地方。

2. 环境准备与record参数的门道

2.1 内核配置与perf安装的坑

在动手之前,先确认你的内核开了调度事件追踪。大部分发行版默认是开的,但有些精简过的内核会裁掉。检查方法很简单:

# 查看调度相关tracepoint是否存在 ls /sys/kernel/debug/tracing/events/sched/

如果能看到sched_switch、sched_wakeup、sched_wakeup_new、sched_process_exit这些目录,说明内核支持。如果这个路径不存在,先挂载debugfs:

mount -t debugfs none /sys/kernel/debug

perf本身的安装,Debian/Ubuntu系是apt install linux-tools-common linux-tools-$(uname -r),RHEL/CentOS系是yum install perf。这里有个经典坑:perf的版本必须和内核版本匹配,否则record的时候会报Perf event open failed或者干脆记录不到调度事件。我见过有人用5.4的perf去分析6.1的内核,结果sched_switch事件一个都抓不到,折腾半天以为是权限问题。所以装完先跑一下:

perf version uname -r

两个版本号对不上就换对应的包。

还有一个权限问题。perf sched record需要CAP_SYS_ADMIN或者perf_event_paranoid足够低。临时放开:

# 查看当前值,默认可能是2或3 cat /proc/sys/kernel/perf_event_paranoid # 临时设为-1,允许所有事件采集 echo -1 > /proc/sys/kernel/perf_event_paranoid

注意:生产环境不要长期把perf_event_paranoid设成-1,采集完记得改回去。这个参数放开意味着任何用户都能采集系统级事件,有信息泄露风险。

2.2 record命令的参数怎么选

perf sched record是整个分析流程的入口,它负责把调度事件抓下来存成perf.data。最基础的用法:

perf sched record -- sleep 10

这行命令的意思是:开始记录调度事件,同时运行sleep 10,10秒后sleep结束,record自动停止。你也可以用-p指定进程、用-a抓全系统、用-C指定CPU。但真正影响分析质量的是下面这几个参数,我一个个说。

-g(抓调用栈):这个参数会记录每次调度事件发生时的内核调用栈。好处是你能看到是谁触发了这次调度(比如是mutex_lock还是schedule_timeout),坏处是数据量会暴涨好几倍。我的经验是:第一轮排查先不加-g,先用latency定位到可疑任务,第二轮再针对那个任务加-g抓栈。一上来就加-g,perf.data动辄几百MB,分析起来很痛苦。

--filter(事件过滤):如果你只关心某个特定任务,可以过滤:

perf sched record --filter 'comm == "myapp"' -- sleep 10

这样只记录myapp相关的调度事件,数据量小很多。但要注意,过滤之后你就看不到其他任务对myapp的干扰了,所以定位阶段不建议用,确认阶段再用。

-R(原始trace输出):这个参数让record把原始事件直接打到stdout,而不是存文件。适合快速看一眼事件流,但不适合做统计分析。

采样时长怎么定:这是很多人纠结的问题。我的建议是至少覆盖3到5个问题复现周期。比如你的应用每30秒卡一次,那就至少录150秒。录太短可能刚好错过毛刺,录太长数据又太大。可以先录60秒看看latency的分布,如果毛刺没抓到,再延长。

2.3 一个完整的record示例

假设我要分析一个叫worker的进程为什么延迟高,我会这样操作:

# 第一步:确认worker的PID pgrep worker # 假设输出 12345 # 第二步:抓60秒,同时抓调用栈,只抓worker相关 perf sched record -g --filter 'comm == "worker"' -- sleep 60 # 第三步:先看latency汇总 perf sched latency --sort max # 第四步:如果发现某个延迟特别大,用timehist看细节 perf sched timehist -p 12345

这套流程我用了很多次,基本能覆盖大部分场景。下面几节我会把每个分析命令的输出逐列拆开讲。

3. latency视图:每个任务的调度延迟账本

3.1 输出列的含义逐项拆解

perf sched latency的输出长这样(我简化了一下):

-------------------------------------------------------------------- Task | Runtime | Switches | Average delay | Maximum delay | Maximum delay at -------------------------------------------------------------------- worker:12345 | 12.345 ms | 150 | 0.082 ms | 15.234 ms | [timestamp] kworker/0:1:678 | 3.210 ms | 80 | 0.015 ms | 0.890 ms | [timestamp] --------------------------------------------------------------------

每一列的含义:

  • Runtime:这个任务在采集窗口内实际占用CPU的总时间。注意这是"上CPU跑"的时间,不包括等待。
  • Switches:这个任务被调度上CPU的次数。次数多说明任务频繁被唤醒和切换。
  • Average delay:平均调度延迟,即任务从"可运行"状态到"真正上CPU"的平均等待时间。
  • Maximum delay:最大调度延迟,这是最关键的指标,毛刺就藏在这里。
  • Maximum delay at:最大延迟发生的时间戳,方便你回去看那个时刻发生了什么。

这里要重点解释一下调度延迟(sched delay)的定义:当一个任务被唤醒(或者时间片用完重新入队)变成可运行状态,到它真正被调度器选中上CPU执行,这中间的等待时间就是调度延迟。它和"唤醒延迟"不是一回事——唤醒延迟是从硬件中断触发到任务被标记为可运行,调度延迟是从可运行到真正运行。perf sched latency统计的是后者。

3.2 怎么读这些数字

拿到输出后,我的阅读顺序是这样的:

第一步,看Maximum delay排序。加--sort max参数,让延迟最大的任务排在最前面。如果某个任务的Maximum delay是几十毫秒甚至上百毫秒,而Average delay只有零点几毫秒,说明这个任务平时很规律,但偶尔被严重阻塞。这种"平均值正常、最大值离谱"的模式,通常意味着优先级反转或者CPU被长时间占用。

第二步,看Switches和Runtime的比例。如果Switches很高但Runtime很短,说明任务被频繁切换,可能是锁竞争激烈或者被高优先级任务反复抢占。如果Runtime很长、Switches很少,说明任务一旦上CPU就霸占很久,可能是它自己不让出CPU。

第三步,对比同类任务。比如你有10个worker线程,其中9个的Maximum delay都在1ms以内,只有1个到了20ms,那问题大概率出在那个线程绑定的CPU上,或者它和其他重负载任务共享了CPU。

3.3 一个真实的排查案例

之前遇到过一个场景:一个数据处理服务,平时P99延迟5ms,但每隔几分钟会飙到200ms。用perf sched latency --sort max一看,Maximum delay 198ms,Average delay 0.3ms。时间戳指向某个时刻,用timehist回看那个时刻,发现是一个kworker在做内存回收(kswapd相关),把CPU占住了将近200ms。

这个案例的教训是:调度延迟的毛刺往往不是你的应用自己造成的,而是系统级的后台任务。所以分析的时候不要只盯着自己的进程,latency输出里那些kworker、migration、rcu_*线程的延迟同样值得关注。

实操心得:perf sched latency默认只显示有调度事件的任务。如果某个任务完全没出现,要么它没被调度过(不可能),要么record的时候过滤条件把它排除了。检查一下你的--filter是不是写错了。

4. timehist视图:逐事件的调度时间线

4.1 timehist和latency的区别

如果说latency是"汇总账本",那timehist就是"流水账"。它把每一个调度事件按时间顺序列出来,包括任务唤醒、上CPU、下CPU、迁移等。当你从latency定位到某个时间点有问题,就需要用timehist去看那个时间点前后到底发生了什么。

基础用法:

perf sched timehist

输出大概是这样:

time cpu task name wait time sch delay run time [tid/pid] (msec) (msec) (msec) --------------- ------ ------------------------------ --------- --------- --------- 1234.567890 [0001] worker[12345] 0.000 0.000 0.500 1234.568390 [0001] kworker/1:2[678] 0.120 0.050 0.200 1234.568590 [0001] worker[12345] 0.200 0.080 0.300

4.2 三个时间列的区别

这是最容易搞混的地方,我重点讲:

  • wait time:任务从上一次下CPU到这一次被唤醒之间的时间。也就是任务处于"睡眠"状态多久了。这个值大说明任务在等IO或者等锁。
  • sch delay:调度延迟,和latency里的定义一样,从可运行到真正上CPU的等待。
  • run time:这次上CPU跑了多久。

理解这三个值的组合,能帮你快速判断问题类型:

wait timesch delayrun time可能的问题
大小正常任务在等IO/锁,不是调度问题
小大正常任务被抢占,CPU竞争激烈
小小大任务自己霸占CPU,需要检查代码
大大大系统整体过载,需要扩容或优化

4.3 用timehist定位毛刺

回到上一节那个198ms延迟的案例。我先用latency拿到最大延迟的时间戳,然后用timehist加上时间过滤:

perf sched timehist --time 1234.5,1234.7

这样只看1234.5到1234.7这段时间的事件。输出里能清楚看到:worker在1234.567被唤醒,但直到1234.765才上CPU,中间这段时间CPU一直被kworker占着。再结合-g抓的调用栈,就能确认是内存回收路径。

timehist还有几个实用参数:

  • -p <pid>:只看指定进程
  • --state:显示任务下CPU时的状态(S睡眠、R运行、D不可中断等)
  • -w:显示唤醒事件,能看到是谁唤醒了任务
  • --max-stack:配合-g使用,限制栈深度

注意:timehist的输出可能非常长,一个繁忙的系统每秒有几千次调度。所以一定要配合--time或者-p过滤,否则刷屏刷到怀疑人生。

5. map视图与其他辅助命令

5.1 map:CPU占用的一目了然视图

perf sched map用字符画的方式展示每个CPU在每个时刻跑的是哪个任务。输出类似:

*A0 A1 . . *B0 . . . . *A0 . . *B0 . . . . .

每个字符代表一个任务,*表示这个CPU刚发生了上下文切换,.表示空闲。这个视图的好处是一眼看出CPU有没有空闲、任务有没有在CPU之间跳来跳去。

用法:

perf sched map # 只看特定时间范围 perf sched map --time 1234.5,1234.6

如果发现某个任务在多个CPU之间频繁出现(比如A0在CPU0、CPU1、CPU2之间反复横跳),说明它没有绑定CPU,缓存局部性差,性能会受影响。这时候可以考虑用taskset绑定。

5.2 replay:调度行为的重放

perf sched replay会模拟重放采集到的调度事件,输出一个"如果按这个调度序列重放,总耗时是多少"的统计。这个命令用得相对少,主要用来做调度器的回归测试。日常排查基本用不上,但知道有这么个东西。

5.3 几个辅助命令

  • perf sched script:把调度事件以脚本形式输出,方便自己写脚本处理。
  • perf sched trace:实时打印调度事件,类似trace-cmd。
  • perf sched stats:输出调度器的统计信息,比如上下文切换总数、迁移次数等。

这些命令我平时用得不多,但script在需要自定义分析的时候很有用。比如你想统计某个任务被迁移了多少次:

perf sched script | grep 'worker' | grep 'migrate' | wc -l

6. 常见问题与排查速查表

6.1 record报错的几种情况

问题一:perf sched record报Permission denied

原因:perf_event_paranoid太高,或者没有root权限。解决:用root跑,或者临时调低perf_event_paranoid。

问题二:record成功但latency输出为空

原因:大概率是--filter写错了,或者采集期间目标进程没有调度事件。检查filter语法,确认进程确实在运行。

问题三:perf.data特别大

原因:加了-g或者采集时间太长。解决:缩短采集时间,或者先不加-g。

问题四:时间戳对不上

原因:perf用的是单调时钟,和date命令的墙上时钟不一样。需要换算的话,用perf script --header看采集起始时间。

6.2 分析时的常见困惑

困惑一:Average delay很低但Maximum delay很高,正常吗?

正常,但要看高到什么程度。如果Maximum是Average的100倍以上,说明有偶发的严重阻塞,值得深挖。如果只是几倍,可能是正常的调度抖动。

困惑二:为什么我的任务延迟不高,但应用还是卡?

调度延迟只是延迟的一部分。应用卡可能是IO等待、锁竞争、内存分配等原因。perf sched只能告诉你调度层面的问题,其他层面要用perf record、strace等工具配合。

困惑三:sch delay和wait time总是分不清

记住一句话:wait time是"睡"了多久,sch delay是"醒"了多久才轮到。睡是主动的(等IO),醒着等是被动的(被抢占)。

6.3 避坑清单

  • 采集前先确认perf版本和内核版本匹配
  • 第一轮排查不要加-g,数据量太大
  • --filter只在确认阶段用,定位阶段会漏掉干扰信息
  • 采集时长至少覆盖3到5个问题周期
  • 分析时不要只看自己的进程,系统后台任务的调度同样重要
  • timehist一定要加时间或进程过滤,否则输出爆炸
  • 生产环境采集完记得恢复perf_event_paranoid

7. 从数据到结论的完整分析路径

7.1 一套可复用的分析流程

我把上面这些串成一套流程,你照着走就行:

  1. 明确问题:先搞清楚是延迟毛刺、吞吐下降还是CPU利用率异常。
  2. 粗粒度采集:perf sched record -- sleep 60,不加-g,不加filter。
  3. 看latency汇总:perf sched latency --sort max,找出Maximum delay最大的几个任务。
  4. 定位时间点:记下最大延迟的时间戳。
  5. 看timehist细节:perf sched timehist --time <起>,<止>,看那个时间点前后的事件。
  6. 看map视图:perf sched map --time <起>,<止>,确认CPU占用情况。
  7. 二次采集:针对可疑任务,加-g和--filter重新采集,抓调用栈。
  8. 结合其他工具:用perf record看CPU热点,用strace看系统调用,交叉验证。

7.2 一个完整的分析记录

假设我分析一个叫encoder的进程,发现它每隔一段时间延迟飙升。按流程走:

第一步,采集60秒:

perf sched record -- sleep 60

第二步,看latency:

perf sched latency --sort max

输出显示encoder的Maximum delay是45ms,Average是0.5ms,时间戳在1234.5附近。

第三步,看timehist:

perf sched timehist --time 1234.4,1234.6

发现encoder在1234.45被唤醒,但直到1234.495才上CPU,中间CPU被kworker/2:1占着。

第四步,看map:

perf sched map --time 1234.4,1234.6

确认CPU2在那段时间确实被kworker占满。

第五步,二次采集抓栈:

perf sched record -g --filter 'comm == "kworker/2:1"' -- sleep 10 perf sched timehist -p <kworker_pid> --max-stack 8

看到调用栈里有shrink_node、do_try_to_free_pages,确认是内存回收导致的。

第六步,结论:encoder的延迟毛刺是内存回收线程抢占CPU导致的。解决方案可以是调整内存回收策略、给encoder绑核、或者提高它的调度优先级。

7.3 参数计算的一个小例子

有人问过:perf sched latency里的Average delay是怎么算的?其实很简单,就是所有调度延迟的总和除以切换次数。假设一个任务被切换了100次,每次的调度延迟加起来是8ms,那Average就是0.08ms。这个值本身意义不大,重要的是它的分布。如果100次里有99次是0.01ms,1次是8ms,那Average会被那一次拉高,但真正的问题就是那一次。所以永远优先看Maximum delay,Average只是参考。

8. 一些压箱底的经验

8.1 关于采集时机的选择

调度问题往往和系统负载相关。如果你在系统空闲的时候采集,可能什么问题都抓不到。我的做法是:在问题复现的时候采集,同时记录当时的系统负载。可以用vmstat 1在旁边跑着,采集结束后对比时间戳。这样能确认问题是不是和负载高峰相关。

8.2 关于多核系统的分析

多核系统上,调度问题会更复杂,因为任务可能在CPU之间迁移。perf sched map在多核场景下特别有用,能直观看到迁移模式。如果发现任务频繁迁移,可以考虑:

  • 用taskset绑定CPU
  • 调整/proc/sys/kernel/sched_migration_cost_ns(增大迁移成本,减少迁移)
  • 检查NUMA配置,避免跨NUMA节点迁移

8.3 关于实时性要求的场景

如果你的场景对延迟极其敏感(比如音视频处理、工业控制),perf sched能帮你确认是否满足实时性要求。重点关注Maximum delay是否超过你的deadline。如果超了,考虑:

  • 用SCHED_FIFO或SCHED_RR实时调度策略
  • 用isolcpus隔离CPU,避免其他任务干扰
  • 用cgroup限制后台任务的CPU占用

8.4 一个容易被忽略的细节

perf sched采集的是调度事件,但调度事件本身也有开销。在高频调度的系统上,采集过程可能会轻微影响系统性能。所以采集时间不要太长,够用就行。另外,采集的时候尽量避开业务高峰期,除非你要分析的就是高峰期的问题。

8.5 关于数据保存和对比

perf.data文件可以保存下来,用perf sched latency -i perf.data离线分析。我习惯把每次采集的数据按"日期-场景"命名保存,比如20240115-encoder-latency.data。这样优化前后可以对比,用数据说话。对比的时候重点看Maximum delay的变化,如果优化后Maximum从45ms降到5ms,那就是实打实的改善。

最后分享一个小技巧:如果你觉得perf sched的输出太原始,可以把它导出成CSV,用表格软件画图。perf sched timehist --csv能输出CSV格式,然后你就能画延迟随时间变化的曲线,毛刺一目了然。这个做法在给团队做汇报的时候特别有用,比一堆数字直观多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询