6BPF之巅:洞悉Linux系统和应用性能

CPU 分析

调度、剖析与火焰图

runqlatprofileoff-CPU 分析CPU 火焰图上下文切换软中断NUMA

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章 CPU 分析

调度、剖析与火焰图

章节导读

CPU 是服务器最昂贵的资源,也是性能分析的第一站。本章建立完整的 CPU 分析体系:从宏观利用率到微观调度行为,从 on-CPU 热点到 off-CPU 等待,从用户进程到中断处理。BPF 工具在这个领域的能力是革命性的——在 BPF 之前,"这个线程为什么等了 50 毫秒才拿到 CPU"这样的问题几乎无法在生产环境回答;有了 runqlat、cpudist、profile、offcputime 这套工具,CPU 行为的每个维度都变成了可测量的。

先建立 CPU 分析的概念框架。一个线程的时间可以完整分解为三部分:on-CPU 时间(正在执行指令)、runnable 等待时间(可运行但在排队等 CPU)、blocked 睡眠时间(等待 I/O、锁、事件)。传统工具只覆盖第一部分(top/ps 的 CPU 利用率)和第三部分的一部分(iostat 的 await 间接反映 I/O 等待),第二部分——调度延迟——在 BPF 工具出现前几乎是盲区。而恰恰是第二部分藏着最多生产事故:CPU 超卖的云主机上邻居抢占、cgroup 配额限流、中断风暴挤占、调度器参数不当、NUMA 迁移开销。BPF 的 runqlat 直接测量第二部分,offcputime 精确测量第三部分,profile 深入第一部分——三件套覆盖线程时间的 100%。

CPU 剖析(profiling)是热点分析的经典技术:以固定频率采样每个 CPU 上正在执行的调用栈,样本数正比于时间占比,聚合后得到热点分布。BPF 的 profile 工具用 perf_event 定时器(默认 99Hz)触发 BPF 程序,调用 bpf_get_stackid 把栈存入内核态的栈表 map 并计数——聚合完全在内核完成,30 秒采集在 64 核机器上产生的输出只有几千行栈记录,开销低于 1%。对比传统方案:gprof 需要重新编译插桩(侵入且失真);perf record 把每个样本的完整栈传回用户态(数据量大,高频采样时丢样本);VTune 等商业工具功能强但部署重。BPF profile 是生产环境持续剖析(continuous profiling)的技术基础——Parca、Pyroscope、Google-Wide Profiling 等系统都基于同样的原理在数十万台服务器上常态运行。

调度延迟分析是本章的技术核心。Linux 的 CFS(完全公平调度器,6.6 前默认)维护每个 CPU 的运行队列(runqueue),线程被唤醒后进入队列,等待调度器选中。runqlat 测量"入队到上 CPU"的延迟:挂载 sched_wakeup(记录唤醒时间戳)和 sched_switch(线程真正开始运行时计算差值)。健康系统的调度延迟主峰在 1-10 微秒;出现毫秒级长尾的可能原因:CPU 供给不足(利用率饱和)、单核热点(中断或单线程霸占)、cgroup 限流(配额用尽被 throttle)、虚拟机 steal(宿主机超卖)、调度器 bug 或参数不当(如 migration 开销)。runqlen 补充队列长度视角,cpudist 补充"单次运行时长"视角——时长分布揭示线程的运行模式:极短(微秒级)说明频繁让出(可能是锁或 I/O 密集),极长(百毫秒级)说明 CPU 霸占者存在。

上下文切换是另一个常被误解的指标。vmstat 的 cs 列显示每秒切换次数,但"切换多"本身不是问题——事件驱动的高并发服务切换频繁是正常设计。需要区分:自愿切换(voluntary,线程主动阻塞,反映 I/O 和锁行为)和非自愿切换(involuntary,时间片耗尽被抢占,反映 CPU 竞争)。pidstat -w 分别显示两者;BPF 的 cpudist 和 schedslower 提供更深的分布视角。切换的直接成本约 1-3 微秒(寄存器保存恢复、TLB 部分失效),间接成本(缓存污染、NUMA 迁移)可能更大。

中断与软中断分析补全 CPU 时间的"系统部分"。top 的 %sys 高企时,hardirqs 和 softirqs 工具分别测量硬中断和软中断的处理时间:按中断类型分解的直方图能定位是网卡(NET_RX)、块设备(BLOCK)、定时器(TIMER)还是 RCU 在消耗 CPU。经典案例:万兆网卡单队列收包,所有软中断集中在 CPU0,该核 100% si 而应用线程被挤到其他核——解决方案是开启 RSS 多队列并配置中断亲和性。这类问题在 mpstat -P ALL 里表现为单核异常,用 softirqs 确认类型,用 /proc/interrupts 确认分布。

最后是 CPU 绑核与 NUMA。现代服务器的内存访问延迟因 NUMA 节点而异(本地 80ns,远程 130ns+),线程在节点间迁移会导致缓存和 TLB 全量失效。numastat 显示跨节点内存访问,BPF 方案可以追踪 sched_migrate_task 事件统计迁移频率和方向,结合 numactl --hardware 的拓扑做绑核决策。容器环境(Kubernetes 的 CPU Manager static 策略、拓扑管理器)把这些决策自动化了,但排查"容器性能不如物理机"时,NUMA 感知仍是必查项。

本章的工具链总结:利用率异常 → mpstat/pidstat 定位到进程 → profile 找 on-CPU 热点 → 利用率不高但延迟高 → runqlat 查调度延迟 → offcputime 查阻塞 → %sys 高 → syscount/softirqs/hardirqs 分解内核时间 → 虚拟化环境 → steal time 与宿主机关联。每一步都有明确的工具和明确的判断标准,这就是体系化 CPU 分析的力量。

核心概念

  • runqlat
  • profile
  • off-CPU 分析
  • CPU 火焰图
  • 上下文切换
  • 软中断
  • NUMA

要点精析

【CPU 时间的完整分解模型】建立分析框架先要会"分账"。一个线程的墙钟时间 = on-CPU + runnable(等 CPU)+ blocked(等事件)。on-CPU 时间又分为 user(用户态指令)、sys(系统调用内核态执行)、以及被中断偷走的部分(hardirq/softirq 记在系统账上)。/proc/PID/stat 的 utime/stime 给出前两者,schedstat(/proc/PID/schedstat 三列:on-CPU 纳秒、runqueue 等待纳秒、时间片数)给出完整分解——第二列就是 runqlat 测量的东西的累计版。全局视角:/proc/stat 的 user/nice/system/idle/iowait/irq/softirq/steal 八项构成 CPU 总账。iowait 是最被误解的项:它表示"CPU 空闲且有未完成 I/O"的时间——CPU 其实闲着,iowait 高说明 I/O 慢而非 CPU 忙;多核下 iowait 的统计还有竞态(不可靠),应视为提示性指标。steal 是虚拟化专属:hypervisor 把物理 CPU 分给别的虚拟机导致的时间损失,steal 持续 >5% 说明宿主机超卖严重,guest 内做任何优化都无济于事,只能找云厂商或迁移。分析的第一步永远是先把账分清楚:问题在哪个分量,就用哪套工具深入。

【runqlat 深入:调度延迟的测量与解读】runqlat 挂载 sched_wakeup/sched_wakeup_new(记录时间戳,键为被唤醒任务)和 sched_switch(任务上 CPU 时计算差值入直方图)。选项:-p 过滤进程、-P 按进程分解、-L 按线程分解、--cgroup 按 cgroup、-m 毫秒单位、-i 间隔输出。解读基准:空闲系统主峰 1-8 微秒;正常负载主峰 10-100 微秒、P99 < 1ms;出现 10ms+ 长尾必须排查。长尾排查决策树:(1) 全 CPU 普遍长尾 → CPU 总供给不足,看利用率,扩容或降载;(2) 个别 CPU 长尾(-P 分解或按 CPU 过滤)→ 单核热点,查中断亲和(/proc/interrupts)、单线程霸占者;(3) 特定 cgroup 长尾 → CPU 配额 throttle,查 /sys/fs/cgroup/cpu/.../cpu.stat 的 nr_throttled;(4) 虚拟机内长尾但 guest 利用率不高 → steal time,宿主机层问题;(5) 周期性长尾(每 100ms 一批)→ cgroup CFS 周期限流的典型指纹(配额在周期初用尽,线程被冻结到下个周期)。runqlat 是 CPU 饱和度的金标准——比利用率灵敏得多:利用率 70% 的系统完全可能有严重的调度延迟(突发负载、亲和性不均)。

【profile 深入:采样剖析的工程细节】profile 的采样频率选择是门学问:99Hz(默认)每 CPU 每秒 99 个样本,30 秒 × 32 核 ≈ 9.5 万样本,足以分辨占比 0.1% 的热点,开销 <1%。为什么是 99 而不是 100?避免与内核 HZ=100/250 的定时器锁频(lockstep)——如果采样频率与系统周期性活动同步,采样点会系统性偏向特定代码位置,产生偏差;99 这样的"错频"值消除共振。同样的理由适用于 999Hz(不要用 1000)。高频采样(-F 999)用于捕捉短暂现象(如某次毛刺期间的热点),开销约 1-3%,可短时使用。栈遍历模式:默认帧指针(fp)模式要求编译保留帧指针(-fno-omit-frame-pointer),断裂时栈只有内核部分或一层用户帧;--call-graph dwarf 用 DWARF 信息遍历(数据量大但准确);LBR(Last Branch Record,Intel 硬件特性)只记录分支历史(深度 32 层,开销最低)。输出模式:默认按"进程+栈"聚合排序输出;-f 折叠格式供火焰图;-p 限定进程;-U/-K 只要用户/内核栈;--folded 配合 -a 全系统采集是持续剖析的标准姿势。profile 的统计本质要牢记:它是采样不是追踪,占比 <0.1% 的函数在 30 秒采集里只有几个样本,不要过度解读。

【off-CPU 分析的完整方法论】offcputime 测量线程阻塞时间及其栈。方法论要点:(1) 何时用——应用延迟高但 CPU 利用率低(<50%),说明时间在等待,off-CPU 分析是唯一直接手段;(2) 阈值设置——-m 1000(1ms)过滤正常短阻塞,聚焦有意义的等待;长时间采集用 -M 上限防止调度器噪声;(3) 输出解读——按"栈×阻塞总时长"排序,注意多线程叠加效应(100 个线程各等 1 秒 = 100 秒总账,墙钟只有 1 秒);(4) 常见模式库——futex_wait=用户锁;rwsem/down_*=内核锁(mmap_sem 竞争常见于频繁 mmap/munmap 或多线程缺页);io_schedule=块 I/O;sk_wait_data=TCP 接收等待;epoll_wait=事件循环(正常);hrtimer_nanosleep=显式睡眠(正常,除非不该睡);freezer=容器冻结;(5) 与 wakeup 分析联动——阻塞栈找到等待点,wakeuptime 找到唤醒者,完整还原依赖链。off-CPU 火焰图与 CPU 火焰图合并成墙钟火焰图是延迟分析的终极视图:一张图看清请求时间的全部去向。注意 offcputime 的开销:它追踪每次 sched_switch,切换频繁的系统(每秒 10 万+ 切换)下开销可达几个百分点,用 -m 阈值和 -p 过滤控制。

【上下文切换:成本、测量与优化】上下文切换的直接成本:保存/恢复寄存器组(约百纳秒级)、内核栈切换、地址空间切换(CR3 写入导致 TLB 失效——PCID 特性缓解了这点)。间接成本更大:新线程的缓存冷启动(L1/L2 几乎全 miss)、分支预测器污染、以及 Spectre 缓解措施开启后的额外开销(KPTI 使系统调用和切换成本增加 30-100%)。测量工具:vmstat 1 的 cs 列(全系统每秒切换);pidstat -w -p PID 1 分自愿/非自愿(voluntary_ctxt_switches / nonvoluntary);cpudist 测单次 on-CPU 时长分布(时长短 = 切换频繁);schedslower(BCC 实验工具)追踪慢调度事件。优化方向:自愿切换多 → 应用层减少同步阻塞(批量 I/O、异步化、减小锁粒度);非自愿切换多 → CPU 竞争,考虑绑核(taskset/cgroup cpuset)、调整 nice/优先级、或扩容。一个反直觉的事实:切换次数多不等于性能差——nginx 每秒数十万切换但吞吐极高,因为每次切换都是"有活干"的高效切换;真正要警惕的是"切换多且每次运行时间极短"(cpudist 主峰在微秒级)的模式,那说明线程在频繁地做无用功让出(锁乒乓、过度细粒度的同步)。

【中断分析:hardirqs 与 softirqs】hardirqs 测量硬中断处理时间(按中断号/设备分解),softirqs 测量软中断处理时间(按类型分解:HI、TIMER、NET_TX、NET_RX、BLOCK、TASKLET、SCHED、RCU 等)。使用场景:mpstat 显示 %irq 或 %soft 异常(>5% 即值得关注,>20% 是严重问题)。经典问题模式:(1) NET_RX 软中断单核饱和——网卡多队列未开启或 RPS 未配置,所有收包处理挤在一个核,表现为该核 100% si、网络吞吐上不去、延迟抖动大;解法:ethtool -L 开多队列 + irqbalance 或手动 smp_affinity 分散中断;(2) BLOCK 软中断高——存储 IOPS 极高(NVMe 场景常见),中断合并(interrupt coalescing)参数可调;(3) TIMER/SCHED 高——定时器密集(大量短超时)或核数极多(调度域遍历成本);(4) RCU 高——内核 RCU 回调积压,常见于连接创建销毁风暴(每个 socket 有 RCU 释放)。softirqs 的输出是"每 CPU 每类型的时间直方图",-d 显示明细。配合 /proc/interrupts(硬中断计数分布)和 /proc/softirqs(软中断计数)交叉验证。中断分析是"%sys/%soft 高"问题的标准入口,网络密集型服务(负载均衡、CDN 节点)的必修课。

【CPU 火焰图:生成、解读与变体】CPU 火焰图标准流程:profile -F 99 -f 30 > out.folded(或 perf record -F 99 -a -g -- sleep 30; perf script | stackcollapse-perf.pl)→ flamegraph.pl out.folded > cpu.svg。解读规则:x 轴是样本占比(字母序排列,非时间序);y 轴是栈深度(下调用上);宽度 = 时间占比;找"平顶宽塔"——顶部宽平的帧是消耗 CPU 的叶子函数,即优化目标。颜色无含义(默认暖色随机)。常见形态:宽平的 libc 帧(memcpy、malloc)→ 内存操作热点;内核帧占比高(sys 部分宽)→ 系统调用密集,查 syscount 深入;[unknown] 大面积 → 符号缺失(strip/无 debuginfo/JIT);单帧超宽且无子帧 → 该函数自身在烧 CPU(循环、计算)。变体家族:差分火焰图(--diff,红增蓝减,版本对比);off-CPU 火焰图(等待分析);墙钟火焰图(合并);内存火焰图(分配栈);热冷翻转(--color=io)。持续剖析系统(Parca、Pyroscope)把火焰图做成时间序列:每个时间窗口一张图,支持"选择任意历史时刻查看当时的热点"——这是 BPF profile 低开销特性催生的新运维范式:CPU 火焰图从"故障时抓一次"变成"永远在线的回放录像"。

【cpudist 与运行模式分析】cpudist 测量任务每次占用 CPU 的时长分布(从 sched_switch 上 CPU 到下次被换出),选项 -p 过滤、-P 按进程、-O 测 off-CPU 时长(阻塞时长分布)。它揭示任务的"运行模式指纹":主峰在 1-10 微秒 → 频繁让出的细粒度任务(锁密集或高频 I/O);主峰在 1-10 毫秒 → 正常的交互式/批处理任务;主峰在 100 毫秒+ → CPU 霸占者(长时间不让出,可能是计算密集也可能是优先级异常)。实战用法:(1) 找霸占者——runqlat 显示调度延迟长尾,cpudist -P 按进程分解,某进程的运行时长分布整体右移(都是长运行)即嫌疑人;(2) 验证优化——把同步 I/O 改成异步后,cpudist 的主峰应左移(每次运行更短、让出更快);(3) 理解负载性质——新接手一个服务,cpudist 十分钟就能建立"它是 CPU 密集还是 I/O 密集"的定量认知。与 runqlat 的关系:runqlat 测"等待上 CPU 的时间"(受害者视角),cpudist 测"占用 CPU 的时间"(占用者视角),两者结合定位 CPU 竞争的供需双方。-O 模式(off-CPU 时长分布)是 offcputime 的轻量替代:不看栈只看时长分布,开销更低,适合常态监控。

【NUMA 与 CPU 亲和性分析】NUMA 架构下每个 CPU 有本地内存节点,远程访问延迟高 50-100%、带宽低。性能症状:应用内存带宽受限但 numastat 显示大量 numa_miss/other_node 分配;线程频繁跨节点迁移导致缓存全冷。分析工具链:numactl --hardware 看拓扑;numastat -p PID 看进程的节点间分配;lscpu -e 看 CPU-节点映射;BPF 方案追踪 sched_migrate_task tracepoint 统计迁移事件(bpftrace -e 'tracepoint:sched:sched_migrate_task { @[args->orig_cpu, args->dest_cpu] = count(); }'),跨节点迁移(orig 和 dest 属于不同节点)频繁即问题。优化手段:numactl --cpunodebind=0 --membind=0 绑定进程;cgroup cpuset 限制容器 CPU 集合与内存节点;内核自动均衡(auto NUMA balancing,/proc/sys/kernel/numa_balancing)会周期性迁移页面到访问节点——它通常有益但在绑核场景应关闭(迁移开销与绑定策略冲突)。容器编排层:Kubernetes 的 Topology Manager + CPU Manager static 策略实现"整核分配 + NUMA 对齐",高性能场景(DPDK、数据库、JVM 大堆)必开。排查口诀:延迟抖动大 + 缓存命中低 + numastat 有跨节点流量 → 查 NUMA 对齐。

【CPU 缓存与微架构分析:BPF 的边界】CPU 分析的最深一层是微架构:缓存命中率、分支预测、指令级并行(IPC)。这层的工具是 PMC(性能监控计数器),perf stat -e cache-misses,branch-misses,instructions,cycles 是入口。BPF 在这层的能力有限但存在:hardware 类型探针(bpftrace 的 hardware:cache-misses:10000 每 1 万次缓存缺失触发)可以按栈聚合硬件事件——"哪些代码路径产生最多缓存缺失"由此可答;perf 的 c2c 工具分析缓存行竞争(false sharing)。IPC 是最有用的单一指标:IPC = instructions/cycles,现代 CPU 理论值 3-4,实际 1-2 算健康,<0.5 说明严重的内存瓶颈(每个指令都在等缓存/内存)。分析路径:IPC 低 → perf stat 分解(cache-misses? branch-misses?)→ 缓存缺失高 → perf c2c 查 false sharing 或 perf mem 查访问模式 → 定位到数据结构布局问题。这已超出本书主线(Gregg 将其归入"超越 BPF 的工具"),但性能工程师应有全景认知:BPF 覆盖系统和应用层,PMC 覆盖微架构层,两层数据交叉验证才能完整解释性能行为。USE 方法在 CPU 维度的完整展开:利用率(mpstat)、饱和度(runqlat)、错误(PMC 的 machine check、dmesg 的 MCE 记录)。

【虚拟化环境的 CPU 分析特殊性】云和虚拟化环境给 CPU 分析增加了三个特殊维度:(1) steal time——guest 的 /proc/stat steal 列和 top 的 st% 显示被 hypervisor 拿走的时间,持续 >2% 即需关注,>10% 说明超卖严重;steal 的特点是 guest 内完全无法优化,runqlat 会显示调度延迟长尾但 guest 内找不到占用者——这个"找不到原因的调度延迟"正是 steal 的指纹;(2) 时钟源——虚拟机的时钟可能基于 hypervisor 模拟(kvm-clock),BPF 的 nsecs 读数在 VM 迁移或暂停后可能跳变,延迟测量出现不可能的负值或巨大值时要怀疑时钟;(3) vCPU 拓扑失真——guest 看到的 CPU 数与物理核的映射可能是多对一(超线程共享、vCPU 浮动),绑核优化在 guest 内的效果取决于 hypervisor 层是否也做了绑定(需要云平台配合,如 AWS 的 dedicated instances)。容器叠加虚拟化(K8s on 云 VM)时问题复合:cgroup 配额 throttle + steal + 调度延迟三层叠加,排查顺序:先看 steal(虚拟化层)→ 再看 cgroup cpu.stat 的 nr_throttled(容器层)→ 最后看 guest 内调度(应用层)。

代码示例

/* 示例1:runqlat 的多维分解 */
sudo /usr/share/bcc/tools/runqlat 10 1          /* 全系统调度延迟直方图 */
sudo /usr/share/bcc/tools/runqlat -P 10 1       /* 按进程分解 */
sudo /usr/share/bcc/tools/runqlat -p 2233 10 1  /* 只看目标进程 */
/* 输出示例:
usecs   : count   distribution
 0 -> 1 : 4521  |*************************************|
 2 -> 3 : 1203  |**********                          |
 8 -> 15:  312  |**                                  |
16 -> 31:   45  |                                    |
 1K -> 2K:   3  |                                    | <- 毫秒级长尾!
*/
runqlat 的三种模式对应排查的三个阶段:全系统直方图先看总体(有长尾吗?),-P 按进程分解找到受害者(谁在等?),-p 聚焦目标进程做持续观察。上面的输出中主峰在 0-3 微秒(健康),但 1K-2K 微秒桶有 3 个事件——毫秒级调度延迟,对延迟敏感业务是不可接受的毛刺。下一步排查:这 3 次长延迟发生在哪些 CPU(用 bpftrace 版本按 cpu 分解)?是否与软中断风暴同时刻(softirqs 对照)?是否在 cgroup 配额边界(cpu.stat 的 nr_throttled 增量)?是否是 VM steal(/proc/stat 对照)?注意 runqlat 统计的是所有任务(包括内核线程),-P 输出中 kworker 的调度延迟长尾通常指向中断处理或回写压力。生产环境常态监控建议:runqlat -P 60 输出接入时序数据库,P99 调度延迟是比 CPU 利用率灵敏得多的饱和度告警指标——利用率 80% 可能毫无问题,而调度延迟 P99 > 10ms 一定有问题。
/* 示例2:bpftrace 版调度延迟——按 CPU 分解定位单核热点 */
sudo bpftrace -e '
tracepoint:sched:sched_wakeup { @qtime[args->pid] = nsecs; }
tracepoint:sched:sched_switch {
    $wait = @qtime[args->next_pid];
    if ($wait) {
        @ns_by_cpu[cpu] = hist(nsecs - $wait);
        delete(@qtime[args->next_pid]);
    }
}'
/* 输出:CPU3 的直方图长尾显著,其他 CPU 正常 → 单核热点 */
这个脚本是 runqlat 的按 CPU 分解版,用于定位"个别 CPU 调度延迟异常"的场景。原理:sched_wakeup 时被唤醒任务的 pid(args->pid)为键记录时间戳,sched_switch 时 next_pid 上 CPU,差值即等待时间,按当前 cpu 聚合。单核长尾的典型原因:(1) 中断亲和不均——cat /proc/interrupts 看某设备的中断是否全部落在该 CPU;(2) 软中断风暴——softirqs -d 按 CPU 分解确认;(3) 内核线程绑定——ksoftirqd/3、kworker/3 等 per-CPU 线程霸占;(4) 实时优先级任务——SCHED_FIFO/RR 任务在该 CPU 上运行(chrt -p 检查,RT 任务会完全压制普通任务);(5) nohz_full/isolcpus 配置异常。找到热点 CPU 后的验证手段:mpstat -P ALL 1 看该 CPU 的 %irq/%soft/%sys 分解;taskset 把嫌疑进程迁离该 CPU 观察长尾是否消失。这个"全系统直方图 → 按维度分解 → 定位异常维度 → 假设验证"的四步法是所有 BPF 分析的通用骨架,本章后续工具都会重复这个模式。
/* 示例3:profile 全系统采集与火焰图 */
sudo /usr/share/bcc/tools/profile -F 99 -a -f 30 > sys.folded
/* 渲染全系统火焰图 */
./FlameGraph/flamegraph.pl --title "System CPU" sys.folded > sys.svg
/* 只看内核栈(分析 %sys 高的问题): */
sudo /usr/share/bcc/tools/profile -F 99 -a -K 30
/* 输出示例:
    2134  cpuidle_enter_state
    1023  native_queued_spin_lock_slowpath   <- 内核自旋锁竞争!
     892  clear_page_erms
*/
全系统 profile(-a)是"%sys 高但不知道高在哪"的标准解法。-K 只输出内核栈,上面的输出直接暴露三个信息:cpuidle 是空闲(正常,可忽略);native_queued_spin_lock_slowpath 占 1023 个样本——内核自旋锁竞争严重,这是内核态 CPU 消耗的常见元凶,下一步用完整栈(不加 -K)看是哪个子系统的锁(页缓存、inode、网络哈希表、futex 哈希桶都可能);clear_page_erms 是页面清零(新内存分配/缺页的成本),占比高说明内存分配频繁,与第 7 章的缺页分析衔接。火焰图模式下这些热点表现为内核帧的宽塔,点击放大可以看到完整的"应用代码 → 系统调用 → 内核路径 → 锁竞争"调用链——这正是火焰图的价值:不仅告诉你热点是什么,还告诉你谁触发了它。持续剖析实践:把 profile -a -f 60 做成 cron 每分钟采集,火焰图时间序列存档,任何历史时刻的 CPU 行为都可回放——故障复盘时"当时 CPU 在干什么"不再是罗生门。
/* 示例4:softirqs 与中断问题定位 */
sudo /usr/share/bcc/tools/softirqs -d 5
/* SOFTIRQ  TOTAL_usecs
NET_RX       45231     <- 网络收包软中断
TIMER         3021
BLOCK         1203
*/
cat /proc/interrupts | grep eth0   /* 硬中断的 CPU 分布 */
mpstat -P ALL 1 3                  /* 每 CPU 的 %soft */
softirqs -d 输出各类型软中断的总耗时(不带 -d 是直方图)。NET_RX 一枝独秀说明网络收包是软中断大头——这本身正常(网络服务器就该如此),关键看分布:mpstat -P ALL 的 %soft 列如果显示 CPU0 是 45% 而其他核接近 0,就是经典的"单核软中断饱和":网卡只有一个接收队列或中断亲和全指向 CPU0,收包处理(协议栈、socket 队列)全部挤在一个核,该核成为吞吐瓶颈,同时其上运行的应用线程被挤压产生调度延迟。修复路径:ethtool -l eth0 查看队列数,ethtool -L eth0 combined N 开启多队列(N ≤ 核数),然后配置中断亲和(irqbalance 或手动写 /proc/irq/N/smp_affinity),或开启 RPS(软件层收包分发,/sys/class/net/eth0/queues/rx-0/rps_cpus)。验证:修复后 mpstat 的 %soft 应均匀分布,网络吞吐和延迟同步改善。BLOCK 软中断高的场景类似(存储中断集中),TIMER/SCHED 高则更多与核数和定时器密度相关。中断分析的口诀:先看总量(softirqs),再看分布(mpstat -P ALL),最后看来源(/proc/interrupts)。
/* 示例5:cpudist——任务运行时长指纹 */
sudo /usr/share/bcc/tools/cpudist -p 2233 10 1
/* on-CPU 时长分布:
usecs   : count   distribution
 1 -> 1 : 8932   |****************************************|  <- 每次只跑1微秒?
 2 -> 3 : 2103   |*********                               |
*/
sudo /usr/share/bcc/tools/cpudist -O -p 2233 10 1   /* off-CPU 时长分布 */
cpudist 的输出揭示任务的运行节奏。上面的分布异常:主峰在 1-3 微秒——线程每次上 CPU 只跑一两微秒就让出,这是"切换风暴"的指纹。可能原因:(1) 锁乒乓——两个线程高频争抢同一把锁,每次拿到锁只做微小工作就被抢走;(2) 过度细粒度的生产者-消费者——每生产一个字节就唤醒消费者;(3) 自旋锁退化为睡眠锁的边界振荡。配合 -O 模式看 off-CPU 时长:如果 off-CPU 也集中在微秒级,说明线程在"高频短阻塞"(futex 快进快出),锁竞争确认;如果 off-CPU 是毫秒级而 on-CPU 是微秒级,则是正常的 I/O 等待模式(等得久、干得快)。优化方向:批量化(batch)——攒一批工作再让出;降低唤醒频率;无锁数据结构。一个真实案例:某消息队列客户端 cpudist 显示微秒级运行模式,根因是每条消息一次 futex 唤醒,改成批量拉取后吞吐提升 8 倍。cpudist 的价值在于它是"运行模式"的定量指纹——接手任何陌生服务,跑一次 cpudist 就能建立它是粗粒度计算型还是细粒度交互型的认知,这个认知决定了后续所有分析的方向。
/* 示例6:追踪 CPU 迁移与 NUMA 跨界 */
sudo bpftrace -e '
#include <linux/sched.h>
tracepoint:sched:sched_migrate_task {
    @migrations[args->orig_cpu, args->dest_cpu] = count();
}
interval:s:10 { print(@migrations); clear(@migrations); }'
/* 结合拓扑判断跨 NUMA 迁移: */
lscpu -e=CPU,NODE   /* 查看 CPU 到 NUMA 节点的映射 */
sched_migrate_task 在调度器把任务从一个 CPU 迁到另一个 CPU 时触发,携带原 CPU 和目标 CPU。迁移本身有成本:目标 CPU 的缓存是冷的(L1/L2 全部重建)、TLB 部分失效、如果跨 NUMA 节点则内存访问延迟跳升。健康的系统迁移频率应该较低且集中在负载均衡窗口;持续高频迁移说明调度器在"抖动"——常见原因:CPU 数量与活跃线程数接近(负载均衡器反复权衡)、cgroup cpuset 配置过窄、RT 任务挤压、或 numa_balancing 的页面迁移联动。分析方法:把 [orig, dest] 对按 lscpu 的 NODE 映射分类,统计跨节点迁移占比——占比高且应用延迟抖动大时,用 numactl 或 cgroup cpuset 做节点绑定,关闭 numa_balancing 对照测试。容器场景的对应问题:K8s 节点上多个 Pod 共享 CPU 池,kubelet 的 CPU Manager 默认(none 策略)允许容器线程在所有核上漂移,高性能 Pod 应配置 static 策略获得独占整核 + NUMA 对齐。这个脚本展示了 BPF 分析调度器行为的基本方法:追踪调度器 tracepoint(sched_migrate_task、sched_move_numa、sched_swap_numa 等),把内核的负载均衡决策变成可见的数据。

实验笔记

本实验构建完整的 CPU 分析工作流。预计耗时 3-4 小时。环境:多核 Linux(4 核+),安装 bcc-tools、bpftrace、sysstat、stress-ng、FlameGraph。虚拟机即可,但注意 VM 的 steal time 会干扰部分实验(正好用来观察虚拟化效应)。

【实验一:CPU 时间分账】(1) 启动 stress-ng 混合负载(--cpu 2 --io 1 --vm 1 --timeout 120);(2) 用 mpstat -P ALL 1 记录每 CPU 的 user/system/iowait/soft/steal 分解;(3) 用 pidstat -u -w 1 记录各进程的 CPU 与切换;(4) cat /proc/PID/schedstat 读取某进程的三元组(on-CPU、runqueue 等待、时间片数),计算其等待占比;(5) 写一段话把四个数据源的信息拼成该系统的完整 CPU 账本。

【实验二:调度延迟全链路】(1) 空闲状态下 runqlat 10 1 记录基线直方图;(2) stress-ng --cpu $(nproc) 打满全部 CPU,再测 runqlat,观察主峰右移和长尾出现;(3) 用示例2的 bpftrace 脚本按 CPU 分解,确认延迟是否均匀;(4) stress-ng --cpu 1(只用一个核)时对比:其他 CPU 上的任务调度延迟应基本不变——验证 CPU 隔离性;(5) 如果有 cgroup v2,创建一个 cpu.max 限制为 20% 的 cgroup,把 stress-ng 放进去,观察 runqlat -P 中它的调度延迟长尾(配额 throttle 的指纹),并 cat cpu.stat 验证 nr_throttled 增长。

【实验三:火焰图完整流程】(1) 编译一个有已知热点的 C 程序(如嵌套循环做矩阵运算 + 一个低效的字符串查找函数),分别用 -O0 和 -O2 -fno-omit-frame-pointer 编译;(2) profile -p PID -f 30 采集并生成火焰图,确认热点函数是最宽的平顶;(3) 对比 -O0 与 -O2 版本的火焰图差异(内联、循环优化的效果可视化);(4) 故意用 -O2 不带帧指针编译,观察火焰图用户栈断裂的样子,再用 --call-graph dwarf(perf 方式)对照修复效果;(5) 做一张差分火焰图:-O0 版本为基线,-O2 版本对比,解读红蓝区域。

【实验四:off-CPU 与阻塞分析】(1) 写一个程序:主线程 sleep 为主,工作线程交替做磁盘读(direct I/O)和锁等待;(2) offcputime -p PID -f -m 1000 10 生成 off-CPU 火焰图,确认能看到 io_schedule(磁盘等待)和 futex_wait(锁等待)两个宽塔;(3) 用 cpudist -O 对照阻塞时长分布;(4) wakeuptime -p PID 5 观察唤醒者。写出该程序"墙钟时间分解报告":on-CPU 占比(profile)、磁盘等待占比、锁等待占比、显式睡眠占比,四者之和应约等于 100%。

【实验五:中断与软中断】(1) cat /proc/interrupts 记录你的系统中断分布,找出最活跃的设备;(2) softirqs -d 5 记录软中断类型分布;(3) 用 ping -f 或 iperf3(如有网络对端)制造网络负载,观察 NET_RX 软中断增长及其 CPU 分布;(4) 用 hardware 探针统计缓存缺失:bpftrace -e 'hardware:cache-misses:1000000 { @[ustack] = count(); }',观察哪些用户栈触发最多缓存缺失(跑你的矩阵程序对照)。

【实验报告要求】五个实验的数据、图表(火焰图 SVG 截图)、解读结论。核心交付物:实验二的"负载变化 → 调度延迟变化"对照分析,和实验四的墙钟时间分解报告。附加思考:(1) 为什么 runqlat 在 CPU 打满时长尾恶化不是线性的?(提示:排队论,利用率接近 100% 时等待时间趋向无穷。)(2) 你的虚拟机 steal time 是多少?它如何影响以上所有测量的可信度?

延伸阅读

  • Brendan Gregg, "BPF Performance Tools" 第 6 章——本章原始出处,含 CPU 分析完整工具表(传统 + BPF)、调度器内部流程图解和更多火焰图案例。
  • Brendan Gregg, "Systems Performance" 第 2 版第 6 章 (CPUs)——CPU 子系统的百科全书式讲解:调度器原理、PMC 微架构分析、缓存行为、绑核策略,与本书第 6 章互为深浅两层。
  • Linux 调度器文档 Documentation/scheduler/ 目录——sched-design-CFS.rst(CFS 设计)、sched-domains.rst(调度域与 NUMA 均衡)、sched-eevdf.rst(6.6+ 的新调度器),理解 runqlat 数据背后的调度器行为。
  • Brendan Gregg, "CPU Flame Graphs" 与 "The Flame Graph" (brendangregg.com)——火焰图的官方方法论页面,含数十张标注了问题模式的真实火焰图案例,解读训练的最佳素材。
  • perf wiki 的 perf-record/perf-stat 页面与 Intel 64 and IA-32 Architectures Optimization Reference Manual 的 PMC 章节——微架构层分析(IPC、缓存、分支预测)的权威参考,超出 BPF 范围但性能工程师必备。
  • Parca (parca.dev) 与 Pyroscope (grafana.com/oss/pyroscope) 文档——基于 BPF profile 的持续剖析系统,学习"火焰图时间序列化"的产品形态和部署实践。
  • LWN 文章 "An introduction to the Linux scheduler" 系列与 EEVDF 调度器合并报道 (lwn.net/Articles/925371)——调度器演进的第一手报道,理解不同内核版本 runqlat 行为差异的背景。
  • Facebook/Meta 工程博客的 BPF 调度器文章与 sched_ext 文档 (docs.kernel.org/scheduler/sched-ext.html)——BPF 可编程调度器的未来方向,从"分析调度"到"改写调度"的跨越。
6/13CPU
0%