第4章 代码热点分析:CPU Profiling工具(async-profiler、perf)使用方法
各位好。做性能优化,最怕什么?我最怕的是凭感觉猜哪里慢。你想想看,一个复杂的SDK跑起来,几千个函数调用堆在一起,靠肉眼去盯日志、加打点,效率实在太低。这时候我们就需要精准的工具,directly告诉你CPU到底花在了哪个函数上。
这一章,我带你玩转两个主流工具:async-profiler 和 perf。它们能采样调用栈,生成火焰图,让你一眼看出热点在哪。说白了,就是给代码拍X光片。
4.1 为什么需要CPU Profiling
先问个问题:假设你的SDK响应慢了,第一步你会做什么?
很多人会去代码里到处加 System.currentTimeMillis()。这种方法虽然直观,但有三个硬伤:
- 侵入式修改,改了代码还得重新部署
- 只能覆盖你认为可能慢的路径,漏掉隐蔽热点
- 高频率打点本身就会降低性能,干扰测量结果
而CPU Profiling采样方式是非侵入的。它每隔几十微秒就记录一次当前CPU在哪里执行,成千上万次采样后,统计出每个函数的占比。完全不用改你的业务代码。
我刚开始做SDK调优时,有个奇怪现象:整体耗时很长,但每个业务函数看起来都很快。后来用perf一采样才发现,原来是频繁调用的底层锁竞争占了30%的CPU。如果没有Profiling,这种问题可能要排查好几天。
4.2 async-profiler:Java生态的首选
如果你做的是Java SDK优化,async-profiler 是我个人最常用的工具。它基于Linux的 perf_event_open 和 AsyncGetCallTrace,支持CPU采样、Wall-clock采样、内存分配热点分析等。而且它自带一个HTML火焰图生成器,非常方便。
4.2.1 安装与基本用法
下载一个二进制包就行,无需编译。
# 下载
wget https://github.com/async-profiler/async-profiler/releases/download/v3.0/async-profiler-3.0-linux-x64.tar.gz
tar -xzf async-profiler-3.0-linux-x64.tar.gz
cd async-profiler-3.0-linux-x64
# 启动CPU采样,持续30秒,生成火焰图
./profiler.sh -d 30 -o flamegraph -f /tmp/profile.html <PID>
嗯,这里要注意:第一个参数 -d 30 是会阻塞当前终端的,直到采样完成才退出。如果你需要后台运行,可以用 start 子命令,然后再 stop 收集结果。
我的一个经验:在压测时启动采样,频率建议设为 -e cpu(默认)100Hz,也就是每秒采样100次。采样时间太短(比如3秒)可能采样数不够,统计结果有偏差;太长(比如5分钟)又会生成巨大的火焰图,不好分析。我通常先跑15秒预览一下,根据火焰图的高度决定是否延长。
4.2.2 两种采样模式对比
| 模式 | 触发方式 | 适用场景 |
|---|---|---|
CPU 采样(-e cpu) | CPU时间片耗尽时采样 | CPU密集型任务,找计算热点 |
Wall 采样(-e wall) | 固定时间间隔采样,无论CPU是否忙碌 | I/O等待、锁竞争、网络阻塞 |
举个例子。有一次我发现SDK的某个接口平均响应时间有800ms,但CPU占用却只有20%。用CPU采样怎么也找不到热点。后来换成Wall模式,火焰图立刻暴露了:一个数据库连接池获取操作的等待占了600ms。所以,CPU采样只看计算热点,Wall采样才能看清整体延迟分布。
4.2.3 常用参数速查
| 参数 | 含义 | 示例 |
|---|---|---|
-d | 采样时长(秒) | -d 30 |
-f | 输出文件路径 | -f /tmp/sample.html |
-o | 输出格式(flamegraph, jfr, collapsed) | -o flamegraph |
-e | 采样事件(cpu, wall, alloc, lock) | -e wall |
-i | 采样间隔(微秒),默认10000 | -i 5000 |
-XX:+UsePerfData 和 -XX:+UnlockCommercialFeatures 等标志。我踩过这个坑,才去补的release notes。
4.3 perf:系统级的万金油
如果你的SDK底层有C/C++代码、JNI调用,或者干脆就是纯C++写的,那async-profiler就力不从心了。这时候要用Linux自带的 perf。它性能开销极低,几乎不影响程序运行。
4.3.1 使用示例
# 采样CPU事件,生成调用栈记录
perf record -g -p <PID> -- sleep 30
# 查看实时统计(不录文件)
perf top -p <PID>
# 将采样数据转换为可读的火焰图
perf script -i perf.data > out.perf
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > /tmp/perf-flame.svg
-g 表示生成调用栈信息。没有这个参数,你就只能看到哪个函数占用率高,但不知道是谁调了它。这在分析JDK动态代理或框架调用时特别重要。
perf_event_paranoid。我曾在生产容器里忘了加 sudo,结果采了半天什么都采样不到,还报了 No permission to collect stats。建议测试环境先把 echo -1 > /proc/sys/kernel/perf_event_paranoid 加上,或者直接用root用户。
4.3.2 三种控制策略
使用perf时,采样频率不是越高越好。我自己有个经验:对于CPU密集型的SDK,1000Hz(每秒1000次)已经很精准了;但对于I/O频繁的服务,建议降到100-200Hz,否则采样本身会产生额外开销。
| 策略 | 采样频率 | 推荐场景 |
|---|---|---|
| 低频采样 | 100 Hz | I/O型、高并发服务 |
| 中频采样 | 500 Hz | 混合型应用 |
| 高频采样 | 1000 Hz | CPU密集型计算热点定位 |
4.4 火焰图解读:从一堆彩色条里找真相
这两个工具生成的火焰图,本质上是一张堆叠起来的调用栈统计图。从上往下看,最顶部是当前正在执行的函数;从下往上看,反映了调用链关系。每个色块的宽度,代表这个函数在所有采样中出现的比例。
读图的几个要点:
- 先看顶部:顶部的宽色块,就是最热的Hotspot。如果某个函数占据了整个火焰的三分之一,那它大概率是性能瓶颈。
- 关注“平顶”:如果一个函数自身(自顶)的宽度很大,说明它自身执行时间很长,而不是调用子函数。这种通常需要内联优化或算法改进。
- 对比颜色:火焰图的颜色一般不是按深浅来区分的,而是按调用栈层级随机着色。同一色彩不代表同一函数,所以别被颜色干扰。
记得有一次,我看到火焰图上有两个宽大的“山峰”,一个是序列化函数,另一个是网络IO。我起初以为是网络慢,但仔细看顶部,序列化函数直接占了CPU的40%,它的子调用才5%。结果发现是代码里用了 Jackson ObjectMapper 的 toString() 频繁反射,改成预编译的序列化方案后,CPU占用直接降了30%。
4.5 实践技巧与调优案例
选择工具的标准,我总结成一句话:Java层用async-profiler,底层C/JNI用perf,需要Wall时间就用Wall模式。
下面说几个避坑点,都是我曾经踩过的:
- 采样期间不要让进程空闲。如果你对空闲的服务做CPU profiling,那火焰图上只会看到一堆
Thread.sleep或者epoll_wait,毫无价值。请确保压测流量一直在发。 - 注意短周期函数。如果一个函数执行非常快(比如1微秒),但被调用了上亿次,采样器可能根本采不到它。这时候应该改用 perf 的
--call-graph dwarf提升采样深度,或者直接看火焰图的整体趋势。 - 不要单次采样就拍板。我习惯最少采样3次,每次30秒以上,对比火焰图的一致性。如果某个热点每次都出现,那就是真热点;如果只是偶发出现,可能是 JIT 或 GC 导致的波动。
核心原则:CPU Profiling是找出局部热点的利器,但别忘了结合全局视角。火焰图告诉你“哪里慢”,但具体怎么改,还得回到代码上下文。多问自己一句:“这个热点是真的计算瓶颈,还是因为锁、I/O、GC等间接引起的?”
好了,这一章我们重点讲了热点分析的思路和工具。从async-profiler的Java采样,到perf的系统级抓栈,再到火焰图的解读技巧——这些工具用熟了,你会发现定位性能瓶颈就不再是猜谜,而是有准确数据支撑的科学过程。