文档目录

4.2 两类现象,两条路径

上一节:4.1 负载生成器 | 下一节:4.3 async-profiler


一句话结论

剖析工具选错的代价,是得到「没有瓶颈」这个错误结论。 记住一条分叉:CPU 高 → 用 on-CPU 采样;CPU 不高但慢 → 用 wall-clock 采样。 这两条路径的差别,比工具之间的差别重要得多。


一、用厨房理解两种「慢」

一家餐厅出菜慢,有两种完全不同的原因:

现象 厨房状态 类比后端
灶台全开着,厨师忙不过来 CPU 时间被占满 CPU 高:真在算
厨师都闲着,在等食材送到 CPU 空闲,时间花在等待 CPU 不高:在等 IO / 锁 / 下游

关键洞察:这两种情况的排查手段完全不同。

  • 第一种:看「谁在占用灶台」→ on-CPU 采样,找最宽的函数。
  • 第二种:看「厨师在等什么」→ wall-clock 采样,找阻塞点。

如果对第二种情况用第一种工具:你会看到一张几乎空白的火焰图(因为 CPU 没在干活),然后得出「没有瓶颈」的错误结论。

这是新手最常见的分析错误,也是最浪费时间的一种——因为你会反复采集、反复看空图,怀疑是工具坏了。


二、分叉决策表

现象 首选工具 事件类型 你会看到
CPU 接近饱和(> 70%) async-profiler / JFR cpu / jdk.ExecutionSample 火焰图顶部有明确的宽块
CPU 低(< 40%)但延迟高 async-profiler wall 线程大面积停在等待栈上
CPU 的 sys 占比高 系统工具 + 火焰图 cpu(看内核栈) 系统调用、锁、页错误
GC 停顿与延迟尖刺对齐 GC 日志 / JFR jdk.GCPhasePause 周期性停顿
分配速率高 async-profiler alloc 谁在制造垃圾
吞吐上不去但 CPU 不高 async-profiler lock 锁争用
所有指标都正常但用户说慢 看测量点(第 1 章 1.3) 客户端/网关/tracing 问题可能在网络或上游

三、四种事件的完整分工

事件 采样依据 回答 典型发现
cpu CPU 时间 谁在消耗 CPU? 热点函数、算法问题、序列化开销
wall 墙上时间 时间都花在哪(含等待)? 阻塞 IO、锁等待、下游慢
alloc 分配的对象 谁在制造 GC 压力? 装箱、字符串拼接、正则编译
lock 锁等待时间 谁在抢锁? 锁竞争、临界区过大

记忆方法:

cpu   → 忙的人在干什么
wall  → 所有人(含闲着的)都在干什么   ← 最容易被忽略,也最有用
alloc → 谁在制造垃圾
lock  → 谁在互相等

四、一个常见的错误分析流程

❌ 错误流程:
① 发现 P99 高
② 采 CPU 火焰图
③ 图很空
④ 结论:「没有热点,可能是网络问题」
⑤ 开始查网络、查网关、查压测机……
   花了几个小时,最后发现是数据库慢查询

✅ 正确流程:
① 发现 P99 高,先看 CPU 利用率
② CPU 只有 30% → 时间是花在等待上的
③ 采 wall-clock 火焰图
④ 看到大量线程停在 socketRead0 / JDBC 调用
⑤ 结论:在等数据库 → 去查 SQL(第 7 节)

第 ② 步是关键分叉。这一个判断就能省下几个小时。


五、还要注意「两者的结合」

有些问题只在两张图对照时才看得出来:

对照 结论
cpu 图有热点,wall 图也有同样热点 纯 CPU 瓶颈,优化该函数
cpu 图空,wall 图有大量等待栈 等待类瓶颈(IO/锁/下游)
cpu 图有热点,但 wall 图里这个热点占比很小 该热点不是瓶颈——它很忙但不影响整体延迟(可能是并行的)
两张图都「很空」 可能是:① 采样没生效(权限问题);② 请求量太小;③ 瓶颈在这个进程之外(网关/网络/压测机)

最后一行特别重要:「两张图都空」往往不是「没问题」,而是「问题不在这个进程里」。


六、本节小结

  1. 选错剖析路径的代价,是得到「没有瓶颈」的错误结论。
  2. 分叉点:CPU 高 → cpu 事件;CPU 不高但慢 → wall 事件。
  3. 四种事件的分工:cpu 看谁在忙、wall 看谁在等、alloc 看谁在制造垃圾、lock 看谁在抢锁。
  4. 只采 CPU 火焰图是新手最常见的错误,而「CPU 不高但很慢」在后端极其常见。
  5. 两张图对照看,比单看一张信息量大得多。两张都空,通常意味着问题不在这个进程里。

七、自测

  1. 一个服务的 CPU 利用率是 22%,P99 是 800 ms。你会先采哪种火焰图?为什么另一种会误导你?
  2. CPU 火焰图上有个函数占了 40% 的宽度,但接口的 P99 只有 50 ms(SLO 是 500 ms)。你该优化这个函数吗?
  3. 你采了 cpu 和 wall 两张图,发现两张都「很空」,几乎没有采样到任何业务代码。请列出至少三种可能的解释。
  1. 先采 wall-clock 火焰图(asprof -e wall)。因为 CPU 只有 22%,说明时间花在等待上(等数据库、等锁、等下游、等网络);这种情况下 CPU 火焰图会显示「空闲」或「很空」,你会错误地得出「没有瓶颈」的结论。wall-clock 采样基于墙上时间,能看到线程阻塞在哪个调用栈上,直接指向等待源。
  2. 不该盲目优化。理由:① 接口 P99 只有 50 ms,远低于 SLO 500 ms——说明当前不是问题(第 3.7 节的「什么时候不该测」);② 火焰图上的宽度代表「CPU 时间占比」,如果这个函数在并行线程上运行(比如后台任务、定时任务、预热代码),它占 40% CPU 但不影响请求延迟;③ 要判断它是否是瓶颈,应该看「wall 火焰图里它占多少」以及「优化它能降低多少 P99」。正确的判断顺序:先确认 SLO 是否有问题 → 有问题才优化;优化前先算「上限收益」。
  3. 可能解释:① 采样没生效:perf 权限不足(perf_event_paranoid 太严),async-profiler 退化为精度很低的模式或采不到用户态栈;② 请求量太小或采样时长太短:采样次数不够,栈没有累积起来(可以延长 -d 时长);③ 瓶颈在这个进程之外:比如网关、网络、压测机、或者上游服务——此时进程内确实「无事可做」,两张图都空是真实反映;④ 另一种可能:采样的是错误的进程(比如采到了 shell 或启动脚本的 PID);⑤ 或者是 JIT 把代码内联/优化后,栈上只剩框架帧,看起来"不像业务代码"。