4.3 配套代码:async-profiler 命令全集与火焰图解读
对应小节:4.3 async-profiler 这份文件是命令速查 + 判读手册,建议放在手边。
一、安装与权限(最容易卡住的一步)
# 下载(以 3.x 为例)
curl -L -o async-profiler.tar.gz \
https://github.com/async-profiler/async-profiler/releases/download/v3.0/async-profiler-3.0-linux-x64.tar.gz
tar xzf async-profiler.tar.gz && cd async-profiler-3.0-linux-x64
# 验证
./asprof --version
# Linux 权限:perf_event_paranoid 必须 <= 1
cat /proc/sys/kernel/perf_event_paranoid
sudo sysctl -w kernel.perf_event_paranoid=1
# 容器内:需要额外 capability 或放宽 seccomp
# docker run --cap-add SYS_ADMIN ...
# 或者(Docker Compose)
# cap_add: [SYS_ADMIN]
# security_opt: [seccomp:unconfined]
如果拿不到 perf 权限:async-profiler 会退化到 itimer 模式(精度较低但可用):
asprof -e itimer -d 60 -f out.html <pid>
「火焰图很空」的头号原因就是权限问题——先检查这一项,再怀疑别的。
二、四类事件的完整命令
PID=$(jcmd | grep app.jar | awk '{print $1}')
# ── ① CPU:谁在消耗 CPU(现象:CPU 高)──────────────────────
asprof -d 60 -e cpu -f cpu.html "$PID"
# 包含内核栈(看系统调用、锁)
asprof -d 60 -e cpu --all-kernel -f cpu-kernel.html "$PID"
# ── ② 墙上时间:谁在等(现象:CPU 不高但慢)⭐ ───────────────
asprof -d 60 -e wall -f wall.html "$PID"
# ── ③ 分配:谁在制造垃圾(现象:GC 频繁)──────────────────
asprof -d 60 -e alloc -f alloc.html "$PID"
# 按分配【字节数】而不是【对象数】统计(更容易发现大对象)
asprof -d 60 -e alloc --alloc=1m -f alloc-bytes.html "$PID"
# ── ④ 锁:谁在抢锁(现象:吞吐上不去)────────────────────
asprof -d 60 -e lock -f lock.html "$PID"
参数速查:
| 参数 | 作用 |
|---|---|
-d 60 |
采样 60 秒 |
-e <event> |
事件类型:cpu / wall / alloc / lock / itimer |
-f <file> |
输出文件 |
-o <format> |
输出格式:html(默认)/ collapsed / flamegraph(svg) / jfr / tree / flat |
-i <ms> |
采样间隔(默认 10ms;调小更精确但开销大) |
--include <pattern> |
只保留匹配的栈 |
--exclude <pattern> |
排除匹配的栈 |
--threads |
按线程分色显示 |
--all-kernel |
包含内核栈(默认只含部分) |
--all-user |
包含所有用户态栈(默认过滤部分 JDK 内部帧) |
--alloc=N |
分配采样的阈值(每分配 N 字节采样一次) |
三、按需过滤(栈很深时非常有用)
# 只看业务代码
asprof -d 60 -e cpu --include "com.example.*" -f cpu-biz.html "$PID"
# 排除框架噪声
asprof -d 60 -e wall --exclude "kotlinx.coroutines.*" --exclude "io.netty.*" -f wall-clean.html "$PID"
# 只看某个方法的调用栈
asprof -d 60 -e cpu --include "*OrderService*" -f cpu-order.html "$PID"
四、输出格式与用途
| 格式 | 命令 | 用途 |
|---|---|---|
| HTML | -o html |
交互式(可搜索、折叠),日常首选 |
| SVG | -o flamegraph |
静态图,方便贴进文档 |
| collapsed | -o collapsed |
文本格式,用于 diff |
| JFR | -o jfr |
可与其他 JFR 一起用 JMC 分析 |
| tree | -o tree |
树形文本,便于 grep |
| flat | -o flat |
扁平列表(按自身耗时排序),找"叶子热点"很方便 |
特别推荐 flat:它直接给出「哪些函数自身耗时最多」,不用在火焰图里找:
asprof -d 60 -e cpu -o flat -f cpu-flat.txt "$PID"
head -30 cpu-flat.txt
五、优化前后做 diff(证明收益的最直观方式)
# 优化前
asprof -d 60 -e wall -o collapsed -f before.txt "$PID"
# ... 应用你的优化 ...
# 优化后(同样负载、同样时长)
asprof -d 60 -e wall -o collapsed -f after.txt "$PID"
# 生成差量图(需要 FlameGraph 工具集)
git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph
./difffolded.pl before.txt after.txt | ./flamegraph.pl > diff.svg
差量图的读法:颜色区分「变多」和「变少」的栈——红色变少的部分就是你优化掉的开销。
六、火焰图判读速查表
六条规则
| 规则 | 说明 |
|---|---|
| 横轴不是时间 | 是采样数(≈ 时间占比),左右无先后关系 |
| 宽度 = 占比 | 越宽说明占用越多 |
| 从下往上找第一个分叉 | 找到明显的宽分支,就是大头 |
| 盯住叶子帧 | 最顶层没有子调用的宽块 = 自己在耗时 |
| 忽略框架帧 | 底部大量 Netty/Ktor/协程调度帧是正常的 |
| 对照两张图 | cpu 有热点但 wall 里占比小 → 不是瓶颈 |
按事件类型的判读要点
cpu 图:
宽而平的顶部块 → 热点函数(优化目标)
GC 线程占比较高 → 分配速率高(接着看 alloc)
__pthread_mutex_lock → 锁竞争(接着看 lock)
sys_* / do_syscall → 系统调用多(IO 或日志)
wall 图 ⭐:
socketRead0 → 等数据库/下游
HikariPool.getConnection → 等连接池(→ 第 4.7 节)
Unsafe.park / LockSupport.park → 等锁/队列
Thread.sleep → 有 sleep
FileOutputStream.write → 同步日志刷盘
alloc 图:
Integer.valueOf / boxed → 装箱
StringBuilder / String.concat → 字符串拼接
Pattern.compile → 正则重复编译
copy$default / data class copy → 对象拷贝
Continuation → 协程高频挂起
byte[] / ByteArray → 缓冲区(可能是 Humongous)
lock 图:
synchronized 块/方法 → 临界区过大
ReentrantLock.lock → 显式锁争用
ConcurrentHashMap 内部 → 热点 key 撞桶
ClassLoader.loadClass → 启动期类加载竞争
七、一个自动采集四类图的脚本
#!/usr/bin/env bash
# tools/profile-all.sh <EXP_ID> <DURATION_SEC>
#
# 在压测进行中运行,一次性采集四类火焰图 + flat 排名。
set -uo pipefail
EXP_ID="${1:?usage: profile-all.sh <EXP_ID> [duration]}"
DURATION="${2:-30}"
DIR="docs/experiments/${EXP_ID}/results"
mkdir -p "$DIR"
PID=$(jcmd 2>/dev/null | grep app.jar | awk '{print $1}')
[ -z "$PID" ] && { echo "❌ 找不到应用进程,请确认应用在运行"; exit 1; }
echo "采集目标 PID=$PID,每类事件 ${DURATION}s"
echo "⚠️ 请确认此时【正在压测】,否则采不到有意义的栈"
echo
for EVENT in cpu wall alloc lock; do
echo "── 采集 $EVENT ──"
asprof -d "$DURATION" -e "$EVENT" -f "$DIR/$EVENT.html" "$PID" 2>&1 | tail -2
# 同时输出 flat 排名(便于快速看热点)
asprof -d "$DURATION" -e "$EVENT" -o flat -f "$DIR/$EVENT-flat.txt" "$PID" 2>&1 | tail -1
done
echo
echo "✅ 采集完成 → $DIR"
echo
echo "快速预览各类事件的 Top 热点:"
for EVENT in cpu wall alloc lock; do
echo
echo "[$EVENT] Top 5:"
head -8 "$DIR/$EVENT-flat.txt" 2>/dev/null | tail -5
done
echo
echo "判读提示:"
echo " cpu 有宽热点、wall 也有同样热点 → 纯 CPU 瓶颈"
echo " cpu 很空、wall 有大量等待栈 → 等待类瓶颈(查 wall-flat.txt)"
echo " 两张都空 → 问题可能不在这个进程(查第 4.2 节自测题 3)"
八、动手改造
| 改动 | 观察什么 |
|---|---|
把 -o html 改成 -o flat 并对比 |
flat 更快找到"自身耗时最多"的函数 |
加 --include "com.example.*" |
框架噪声被过滤,业务热点更清晰 |
用 -i 1(1ms 采样间隔) |
精度提升但开销增加——在压测机上对比应用延迟变化 |
故意在接口里加 Thread.sleep(50) |
cpu 图看不到,wall 图能看到——这是 4.2 节的核心验证 |
| 采两次(优化前后)并做 diff | 差量图直接显示优化掉的开销 |
九、这段代码的局限
- 采样本身有开销(尤其
-i 1和alloc模式)。在生产环境长时间采样会影响被观测系统——这本身就是「观测改变被观测对象」的例子(第 0 章 0.9 节)。 wall采样包含线程空闲时间:如果你的服务有很多空闲线程,wall 图里会有大量"空闲"栈,需要结合线程类型解读。- 内联会导致栈帧丢失:看不到某个方法不代表它没被调用(第 2 章 2.2 节)。
- 容器内需要额外的 capability,且部分云环境限制了
perf_event_open——这时只能退到itimer模式。