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/debugperf本身的安装,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.3004.2 三个时间列的区别
这是最容易搞混的地方,我重点讲:
- wait time:任务从上一次下CPU到这一次被唤醒之间的时间。也就是任务处于"睡眠"状态多久了。这个值大说明任务在等IO或者等锁。
- sch delay:调度延迟,和
latency里的定义一样,从可运行到真正上CPU的等待。 - run time:这次上CPU跑了多久。
理解这三个值的组合,能帮你快速判断问题类型:
| wait time | sch delay | run 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 -l6. 常见问题与排查速查表
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 一套可复用的分析流程
我把上面这些串成一套流程,你照着走就行:
- 明确问题:先搞清楚是延迟毛刺、吞吐下降还是CPU利用率异常。
- 粗粒度采集:
perf sched record -- sleep 60,不加-g,不加filter。 - 看latency汇总:
perf sched latency --sort max,找出Maximum delay最大的几个任务。 - 定位时间点:记下最大延迟的时间戳。
- 看timehist细节:
perf sched timehist --time <起>,<止>,看那个时间点前后的事件。 - 看map视图:
perf sched map --time <起>,<止>,确认CPU占用情况。 - 二次采集:针对可疑任务,加
-g和--filter重新采集,抓调用栈。 - 结合其他工具:用
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格式,然后你就能画延迟随时间变化的曲线,毛刺一目了然。这个做法在给团队做汇报的时候特别有用,比一堆数字直观多了。