11、火焰图入门:FlameGraph工具生成CPU火焰图,解读火焰图

性能优化做到一定程度,你会发现一个尴尬的现实:光靠猜,根本找不到瓶颈

我早年做过一个网络代理服务,上线后CPU飙到90%。我盯着代码看了三天,觉得是锁竞争,又觉得是内存拷贝,改了一堆地方,屁用没有。后来同事甩给我一张图——火焰图。我一看,好家伙,90%的CPU都耗在一个字符串处理函数里,那是我自己写的,效率极低。

从那以后,火焰图就成了我工具箱里的常客。说白了,它就是把CPU的采样数据,画成一张“谁在吃CPU”的全景地图

核心思想:火焰图不是告诉你“代码哪里慢”,而是告诉你“CPU的时间都花在了哪个函数里”。

11.1 火焰图长什么样?

先给你个直观印象。一张标准的CPU火焰图,从上往下看,像一座座“山峰”。

  • y轴(纵向):调用栈深度。最顶层是当前正在执行的函数,往下是它的调用链。
  • x轴(横向):采样数量占比。一个函数占的宽度越大,说明它消耗的CPU时间越多。
  • 颜色:通常随机分配,没有特殊含义。别被颜色骗了,只看宽度。

我习惯把火焰图想象成一座“CPU火山”。山顶是正在喷发的函数,山脚是main函数。哪座山最宽,哪座山就是性能瓶颈。

CPU火焰图结构示意 main() handle_request() process_data() parse_header() compress_data() strtok() memcpy() strlen() memmove() ← 调用栈深度(y轴) 函数宽度 = CPU占用比例(x轴)→ 🔥 最宽的山峰 = 热点函数

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。操作分三步:

  1. 采样:perf record -F 99 -g -p 12345 -- sleep 30
  2. 生成数据:perf script > out.perf
  3. 画图:用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