线上服务突然 CPU 100%、接口响应从 50ms 飙到 5s、内存曲线一路向上不回头——这类”性能突发”最怕凭感觉改代码。pprof 是 Go 与 Java 生态里定位性能瓶颈的标配采样工具,配合火焰图,能让你在十分钟内看清”时间到底花在哪”。本文以真实排障视角,讲透 CPU、heap、goroutine 三类 profile 的采集、火焰图读法与生产落地纪律。
一、先采集,再下结论:别让”感觉”背锅
性能问题的第一铁律是:没有 profile 就没有发言权。很多团队一看到 CPU 高就怀疑”是不是 GC 炸了””是不是某个接口被刷了”,结果改了一圈没用。真正高效的排障路径只有三步:采集 profile → 在火焰图上找最宽的那块 → 回到代码确认。pprof 的采样成本极低(默认按固定频率采样调用栈),对生产服务几乎无感,所以”边跑边采”完全可行。
值得一提的是,pprof 看的是进程内函数级热点,而像 Linux 性能排查实战:top、iostat 与火焰图 里的 perf 看的是系统级 CPU 与内核栈。两者火焰图同源、互为补充:先靠 perf 确认”确实是用户态 CPU 高”,再用 pprof 下钻到具体函数。
二、Go 接入 pprof:三行代码开启
Go 标准库自带 net/http/pprof,只要 blank import 它,就会自动在 /debug/pprof/ 注册一系列路由。最常见做法是在一个独立端口(如 6060)上单独起一个 http server,避免和业务端口耦合。
import (
_ "net/http/pprof" // 引入即注册 /debug/pprof/ 路由
"net/http"
)
func main() {
go func() {
// 仅监听本机,切勿暴露到公网
_ = http.ListenAndServe("127.0.0.1:6060", nil)
}()
// ... 业务启动逻辑
}
开启后访问 http://127.0.0.1:6060/debug/pprof/ 就能看到 profile 列表。生产环境务必只监听 127.0.0.1,通过跳板机或 SSH 隧道访问;直接 0.0.0.0 暴露等于把你的调用栈详情拱手送人。这和给管理端口加 SSL 证书、收紧 Nginx 反向代理 访问控制是一个道理。
三、采集 CPU profile:30 秒采样
CPU profile 通过定时中断(默认 100Hz)采样当前调用栈,汇总成”哪些函数占用了多少 CPU 时间”。采样 30 秒是一个稳妥的起步值:太短噪声大,太长可能错过瞬时峰值但也能平滑波动。
# 采集 30 秒 CPU profile,进入交互式界面
go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30
# 或直接拉取保存到文件,便于事后分析/对比
curl -o cpu.pb.gz "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"
进入交互界面后,几个命令最高频:
(pprof) top10 # 按自身耗时(flat)排前 10
(pprof) top10 -cum # 按累计耗时(cum)排前 10
(pprof) list 包名.函数 # 看某个函数的逐行耗时
(pprof) web # 生成 SVG 火焰图并在浏览器打开
(pprof) png > fg.png # 直接导出火焰图图片
四、读懂火焰图:横轴是采样堆积,纵轴是调用栈
火焰图是 pprof 最直观的产出。每一列是一个采样栈,栈顶在上方、调用方在下方;横向宽度代表该函数占用的采样比例。所以你只需要盯着”最宽的那一块栈顶”——它往往就是热点函数。注意不要被”平铺很宽但单块很窄”误导,要看单块宽度。
| Profile 类型 | 看什么 | 采集路由后缀 |
|---|---|---|
| CPU | 函数占用多少 CPU 时间 | /profile?seconds=30 |
| Heap(内存) | 对象占多少内存、谁分配的 | /heap |
| Goroutine | 当前有多少协程、卡在哪 | /goroutine |
| Mutex | 锁竞争耗时分布 | /mutex |
| Block | 阻塞等待耗时分布 | /block |
五、内存暴涨:heap profile 与 inuse_space
内存”只涨不跌”是第二类经典问题。heap profile 默认看 inuse_space(当前仍在使用的内存),这正是你要找的”泄漏点”。注意别一上来就看 alloc_space——那包含已释放的临时对象,会把注意力引到高频短命对象上,反而掩盖真正的泄漏。
# 采集当前堆快照
go tool pprof http://127.0.0.1:6060/debug/pprof/heap
(pprof) top10 -cum # 累计分配最多的调用链
(pprof) list cache.put # 锁定到具体缓存写入方法
若发现某个全局 map 或 slice 的 put 方法持续累积、没有淘汰逻辑,那就是典型的”无限缓存”。这类问题往往和 OOMKilled:容器内存超限被强杀 直接相关——内存涨到 Limit 就被 K8s 杀掉重启,表现为”周期性重启”。
六、goroutine 泄漏:goroutine profile
接口越来越慢、最后超时,常常不是 CPU 的问题,而是 goroutine 泄漏:每个请求起了一个 goroutine,却因为 channel 没人收、context 没取消而永远挂着。goroutine profile 能直接看出”数量异常增长”。
# 抓取当前 goroutine 栈
go tool pprof http://127.0.0.1:6060/debug/pprof/goroutine
(pprof) top # 看数量最多的栈
# 代码里埋一个趋势观测更稳
import "runtime"
n := runtime.NumGoroutine()
log.Printf("goroutines=%d", n) // 配合监控画趋势线
七、互斥与阻塞:mutex / block profile
有些”慢”不是计算慢,而是等锁、等网络、等 channel。这两类默认关闭,需要主动打开采样开关,否则抓不到数据。
import "runtime"
func init() {
runtime.SetMutexProfileFraction(1) // 1 = 采样 100% 锁竞争
runtime.SetBlockProfileRate(1) // 1 = 采样每一次阻塞
}
# 采集
go tool pprof http://127.0.0.1:6060/debug/pprof/mutex
go tool pprof http://127.0.0.1:6060/debug/pprof/block
八、对比基线:用 -base 看 diff
当问题难以稳定复现,或你想确认”这次上线到底引入了什么新热点”,最佳做法是先采一份正常基线,再采一份异常快照,用 base 对比。差值部分就是新增的耗时来源。
# 正常时
curl -o base.pb.gz "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"
# 异常时
curl -o cur.pb.gz "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"
# 对比
go tool pprof -base base.pb.gz cur.pb.gz
九、Java 侧:async-profiler 一把梭
Java 没有标准库 pprof,但 async-profiler 是事实标准:它基于 AsyncGetCallTrace / perf_events,开销极低,还能直接 attach 到正在运行的进程,无需重启。除了 CPU,它还支持 alloc(分配)、lock(锁)、wall(墙钟时间)。
# 下载并解压 async-profiler
# 采样 30 秒 CPU,输出火焰图
./profiler.sh -d 30 -f flamegraph.html -e cpu <pid>
# 看内存分配热点
./profiler.sh -d 30 -e alloc -f alloc.svg <pid>
# 不重启、直接 attach 到运行中 JVM
jattach <pid> load /path/to/libasyncProfiler.so start,event=cpu,file=fg.html
十、三个真实翻车现场
| 场景 | 现象 | 根因 | pprof 抓到的点 |
|---|---|---|---|
| 定时任务 CPU 100% | 整点后单核打满 | 用户输入拼成正则,触发灾难性回溯 | top 显示正则函数占 80% CPU |
| 内存只涨不跌 | 曲线单调上升直至重启 | 全局 map 做缓存,无容量上限与淘汰 | heap inuse_space 集中在 cache.put |
| 接口越来越慢 | RT 缓升,最终大面积超时 | 每请求起 goroutine 未回收(泄漏) | goroutine 数量随时间线性增长 |
十一、与 Linux perf 的分工
再强调一遍分工:perf 站在操作系统视角,告诉你”CPU 时间花在内核还是用户态、哪个进程”;pprof 站在进程视角,告诉你”用户态时间里哪个函数最贵”。排障顺序通常是先 perf 确认是应用自身的问题,再用 pprof 下钻。两者生成的火焰图长得几乎一样,读法通用。
十二、生产落地 6 条纪律
- 只监听本机:pprof 端口 bind 127.0.0.1,经 SSH 隧道访问,绝不暴露在公网。
- 采样 30 秒起步:太短噪声大,容易把偶发尖峰当常态。
- 火焰图优先:用
web或导出 SVG,肉眼看最宽栈,比 top 文本更快定位。 - 内存先看 inuse_space:alloc_space 会让你误判短命对象。
- goroutine 泄漏看趋势:配合
runtime.NumGoroutine()打点画曲线,比单次快照更稳。 - 难以复现就比 base:正常基线 vs 异常快照 diff,直接暴露新增热点。
把 pprof 当作服务的”标准配置”而不是”救火工具”,性能突发时就不再手忙脚乱。它和 Go 并发实战:goroutine 与 channel 里讲的并发模型是同一套心法——理解协程从哪来、到哪去,再让数据告诉你时间花在哪。下一次 CPU 报警,先 go tool pprof,再说话。




