火焰图进阶:生成内存火焰图、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.plflamegraph.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火焰图的步骤

  1. 用perf或eBPF捕获调度事件:sched:sched_switch
  2. 记录线程被切换出去时的调用栈
  3. 聚合调用栈,生成火焰图

用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火焰图 内存火焰图 Off-CPU火焰图 热力图 perf record FlameGraph BCC/eBPF malloc hook sched_switch 锁竞争分析 heatmap.pl 时间分布 结合使用,全面定位性能瓶颈

这张图把四种火焰图的关系和工具链串起来了。你从中心出发,根据问题类型选择分支,然后找到对应的工具。我个人习惯是:先跑CPU火焰图,如果发现CPU不高但延迟高,就转做Off-CPU火焰图;如果内存持续增长,就做内存火焰图;如果问题有时间规律,就上热力图。

好了,这一章的内容就到这里。火焰图进阶的三种玩法,你可以在自己的项目里试试看。尤其是Off-CPU火焰图,很多人在它上面吃过亏——包括我。


公众号:蓝海资料掘金营,微信deep3321