文档目录

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 模式。