火焰图进阶:生成内存火焰图、Off-CPU火焰图、热力图
火焰图这东西,说实话,第一次看到的时候我挺震撼的。满屏的暖色块,像一座座火山,CPU在哪儿烧得最旺一目了然。但用久了你会发现,光看CPU火焰图是不够的。内存泄漏、I/O等待、锁竞争……这些问题CPU火焰图根本看不出来。
所以这一章,我们来聊聊火焰图的进阶玩法。内存火焰图、Off-CPU火焰图、热力图,这三样东西,是我在实际项目中排查性能问题时最常用的三板斧。
一、内存火焰图:谁在吃内存?
CPU火焰图看的是“谁在占用CPU”,内存火焰图看的是“谁在分配内存”。说白了,就是把malloc、calloc、realloc这些内存分配函数给抓出来,然后按调用栈聚合。
我遇到过最头疼的一个案例:一个服务跑了三天,内存从200MB涨到了2GB。用top看,RSS一直在涨,但不知道是谁干的。后来用内存火焰图一照,发现是一个日志模块在每次请求时都malloc了一块缓冲区,但只有在特定错误码下才free。嗯,这种问题,光靠看代码是很难发现的。
如何生成内存火焰图?
生成内存火焰图,核心思路是hook内存分配函数。Linux下常用的工具有两种:
- perf + 自定义探针:用perf的tracepoint或者uprobe来捕获malloc/free
- Valgrind的massif:带详细调用栈,但性能开销大,适合线下分析
- BCC/eBPF:我最推荐的方式,开销小,适合线上
下面是一个用BCC生成内存火焰图的示例脚本:
#!/usr/bin/python
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
int trace_malloc(struct pt_regs *ctx, size_t size) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_trace_printk("malloc size=%d\\n", size);
return 0;
}
"""
b = BPF(text=bpf_text)
b.attach_uprobe(name="c", sym="malloc", fn_name="trace_malloc")
while True:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
print(msg)
except KeyboardInterrupt:
break
抓取数据后,用FlameGraph工具集里的stackcollapse-bpftrace.pl和flamegraph.pl就能生成SVG了。
关键点:内存火焰图只看分配,不看释放。如果某个函数分配了很多内存但没释放,火焰图上会显示一个很宽的“火苗”。这就是内存泄漏的嫌疑犯。
二、Off-CPU火焰图:线程在等什么?
CPU火焰图只展示线程在CPU上运行时的状态。但很多时候,性能瓶颈不在CPU上,而在I/O、锁、网络等阻塞操作上。这时候就需要Off-CPU火焰图了。
Off-CPU火焰图,说白了就是记录线程被调度出去(off-CPU)时的调用栈,然后聚合展示。它能告诉你:线程在等什么?等了多久?
我记得有一次,一个数据库查询接口的P99延迟很高,CPU火焰图看起来一切正常。后来做了Off-CPU火焰图,发现大部分时间都耗在了futex_wait上——说白了,就是锁竞争。优化了一把锁的粒度,P99直接降了60%。
生成Off-CPU火焰图的步骤
- 用perf或eBPF捕获调度事件:
sched:sched_switch - 记录线程被切换出去时的调用栈
- 聚合调用栈,生成火焰图
用perf的示例:
# 捕获调度事件,记录调用栈
perf record -e sched:sched_switch -ag -- sleep 10
# 生成折叠栈
perf script | stackcollapse-perf.pl > out.folded
# 生成火焰图
flamegraph.pl --title="Off-CPU Flame Graph" out.folded > off_cpu.svg
小技巧:Off-CPU火焰图通常和CPU火焰图配合使用。先看CPU火焰图找热点,再看Off-CPU火焰图找阻塞点。两者结合,才能全面了解性能瓶颈。
三、热力图:时间维度的性能分析
火焰图是静态的,它展示的是“累计时间”。但性能问题往往有时间特征——比如某个函数在高峰期才变慢,或者某个锁在特定时间段才激烈竞争。这时候,热力图就派上用场了。
热力图,本质上是一个二维直方图。X轴是时间,Y轴是延迟或采样值,颜色深浅代表样本密度。它能帮你发现:
- 延迟是否随时间变化?
- 是否有周期性抖动?
- 某个操作是否只在特定条件下变慢?
我曾经用热力图排查过一个诡异的问题:一个服务的响应时间每30秒抖动一次。CPU火焰图看不出异常,Off-CPU火焰图也看不出。后来用热力图一看,发现抖动时间点和日志轮转的时间点完全重合。原来是日志文件切换时,写日志的线程被阻塞了。
如何生成热力图?
生成热力图,我一般用两个工具:
| 工具 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| perf timechart | 系统级事件 | 内核自带,无需额外安装 | 粒度较粗 |
| FlameGraph的heatmap.pl | 自定义数据 | 灵活,可定制 | 需要自己准备数据 |
| Grafana + Prometheus | 长期监控 | 可视化效果好 | 部署复杂 |
用FlameGraph工具集生成热力图的示例:
# 假设你有一个包含时间戳和延迟的数据文件
# 格式:timestamp latency
cat latency_data.txt | \
awk '{print int($1/1000), int($2/1000)}' | \
heatmap.pl --title="Latency Heatmap" \
--xlabel="Time (seconds)" \
--ylabel="Latency (ms)" \
> latency_heatmap.svg
注意:热力图的数据量通常很大。如果采样点太少,热力图会显得稀疏,看不出规律。我一般建议至少采集10万以上的样本点,才能得到有意义的分布图。
四、三种图的对比与选择
这三种图各有侧重,我整理了一个对比表,方便你根据场景选择:
| 类型 | 关注点 | 典型问题 | 推荐工具 |
|---|---|---|---|
| CPU火焰图 | CPU占用 | 热点函数、死循环 | perf + FlameGraph |
| 内存火焰图 | 内存分配 | 内存泄漏、频繁分配 | BCC/eBPF |
| Off-CPU火焰图 | 阻塞等待 | 锁竞争、I/O等待 | perf + sched_switch |
| 热力图 | 时间分布 | 周期性抖动、延迟波动 | FlameGraph heatmap.pl |
五、知识体系总览
下面这张图,是我自己总结的火焰图进阶知识体系。你可以把它当作一个快速索引:
这张图把四种火焰图的关系和工具链串起来了。你从中心出发,根据问题类型选择分支,然后找到对应的工具。我个人习惯是:先跑CPU火焰图,如果发现CPU不高但延迟高,就转做Off-CPU火焰图;如果内存持续增长,就做内存火焰图;如果问题有时间规律,就上热力图。
好了,这一章的内容就到这里。火焰图进阶的三种玩法,你可以在自己的项目里试试看。尤其是Off-CPU火焰图,很多人在它上面吃过亏——包括我。