11、火焰图入门:FlameGraph工具生成CPU火焰图,解读火焰图
性能优化做到一定程度,你会发现一个尴尬的现实:光靠猜,根本找不到瓶颈。
我早年做过一个网络代理服务,上线后CPU飙到90%。我盯着代码看了三天,觉得是锁竞争,又觉得是内存拷贝,改了一堆地方,屁用没有。后来同事甩给我一张图——火焰图。我一看,好家伙,90%的CPU都耗在一个字符串处理函数里,那是我自己写的,效率极低。
从那以后,火焰图就成了我工具箱里的常客。说白了,它就是把CPU的采样数据,画成一张“谁在吃CPU”的全景地图。
核心思想:火焰图不是告诉你“代码哪里慢”,而是告诉你“CPU的时间都花在了哪个函数里”。
11.1 火焰图长什么样?
先给你个直观印象。一张标准的CPU火焰图,从上往下看,像一座座“山峰”。
- y轴(纵向):调用栈深度。最顶层是当前正在执行的函数,往下是它的调用链。
- x轴(横向):采样数量占比。一个函数占的宽度越大,说明它消耗的CPU时间越多。
- 颜色:通常随机分配,没有特殊含义。别被颜色骗了,只看宽度。
我习惯把火焰图想象成一座“CPU火山”。山顶是正在喷发的函数,山脚是main函数。哪座山最宽,哪座山就是性能瓶颈。
11.2 怎么生成火焰图?
生成火焰图,你需要两样东西:采样工具和FlameGraph脚本。
11.2.1 采样工具选哪个?
| 工具 | 适用场景 | 命令示例 |
|---|---|---|
| perf | Linux原生,最通用 | perf record -F 99 -g -p PID |
| DTrace | macOS / Solaris | dtrace -x ustackframes=100 -n '...' |
| SystemTap | 老版本Linux | 略复杂,不推荐新手 |
我个人最常用perf。原因很简单:99%的服务器都是Linux。采样频率我习惯设99Hz,为什么不是100?因为100Hz容易和某些定时器产生共振,导致采样偏差。99Hz是个安全值。
小技巧:采样时间不要太短。我一般跑30秒以上。时间太短,采样点不够,火焰图会“稀疏”,看不出趋势。
11.2.2 完整操作流程
假设你要分析一个正在运行的进程,PID是12345。操作分三步:
- 采样:
perf record -F 99 -g -p 12345 -- sleep 30 - 生成数据:
perf script > out.perf - 画图:用FlameGraph脚本转换
FlameGraph是Brendan Gregg大神写的开源工具。你从GitHub克隆下来就行:
git clone https://github.com/brendangregg/FlameGraph.git
cd FlameGraph
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > flame.svg
嗯,这里要注意:flamegraph.pl生成的SVG是可交互的。鼠标悬停能看到函数名和占比,点击可以放大。我经常在开会时打开SVG,指着最宽的那块说:“看,就是这里在吃CPU。”
11.3 怎么解读火焰图?
拿到一张火焰图,别急着看细节。先问自己三个问题:
- 山顶是什么?最顶层是实际占用CPU的函数。
- 哪座山最宽?宽度代表CPU时间占比。
- 山有多高?调用栈深度。太深可能意味着过度抽象。
11.3.1 常见模式
我总结了三种最常见的“坏味道”:
模式一:平顶山
山顶很宽,但调用栈很浅。比如一个while循环里直接调用usleep()。这说明CPU在空转或忙等。
模式二:尖塔山
山顶很窄,但山脚很宽。比如malloc()占了很多CPU。这说明内存分配是瓶颈,你可能需要对象池。
模式三:锯齿山
调用栈忽高忽低,像锯齿。这通常意味着锁竞争。线程在等待锁时,CPU会空转,采样点落在锁函数上。
我曾经遇到过一个案例:火焰图显示pthread_mutex_lock()占了40%的CPU。我一开始以为是锁粒度太大,后来仔细看调用栈,发现是锁顺序不一致导致的死锁重试。嗯,这个坑我踩过,分享给你。
11.3.2 避坑指南
我曾经犯过的错:
- 只看顶层:有一次我盯着
memcpy()优化了半天,后来发现是上层调用传了过大的缓冲区。顶层函数只是“受害者”,不是“元凶”。 - 采样频率太低:用1Hz采样,等了10分钟,火焰图只有几个点。根本看不出规律。99Hz是黄金频率。
- 忽略内核栈:有些性能问题出在内核态。记得加
-k参数采集内核调用栈。
11.4 实战:用火焰图定位一个假想问题
假设你有一个C程序,功能是处理网络请求。你发现CPU高,但不知道哪里慢。我们来模拟一下:
// 模拟代码:处理请求
void handle_request(int fd) {
char buf[4096];
read(fd, buf, sizeof(buf)); // 网络读取
parse_request(buf); // 解析请求
process_data(buf); // 处理数据
write(fd, response, len); // 返回响应
}
void parse_request(char *buf) {
// 这里有个低效的字符串分割
char *token = strtok(buf, " ");
while (token) {
// 处理每个token
token = strtok(NULL, " ");
}
}
你运行perf record -F 99 -g -p $(pgrep myapp) -- sleep 30,生成火焰图后,发现:
strtok()占了60%的宽度- 调用栈是
main -> handle_request -> parse_request -> strtok
结论很明显:瓶颈在字符串解析。strtok每次调用都要遍历字符串,O(n²)复杂度。优化方案:改用strchr手动分割,或者用状态机解析。
我的建议:优化后重新采样,对比两张火焰图。如果那座“山”变矮了,说明优化有效。如果没变,继续找下一座山。
11.5 火焰图的局限性
任何工具都有短板。火焰图也不是万能的:
- 不适用于I/O密集型:如果程序在等磁盘或网络,CPU是空闲的,火焰图看不出问题。这时候你需要Off-CPU火焰图。
- 采样偏差:短函数可能被漏采。如果函数执行时间小于采样间隔(约10ms),它可能永远不会出现在火焰图上。
- 无法定位内存问题:火焰图只看CPU,不看内存。内存泄漏需要用Valgrind或ASan。
说白了,火焰图是CPU性能分析的第一站,但不是终点。它帮你快速找到“谁在吃CPU”,但具体怎么优化,还得靠你对代码的理解。
我个人习惯是:拿到火焰图后,先截图,然后标注出最宽的3个热点。接着打开代码,逐个分析。优化完一个,重新采样,再看火焰图。如此循环,直到性能达标。
公众号:蓝海资料掘金营,微信deep3321