告别 CPU 飙高与内存泄漏:我的 pprof 实战排查与避坑笔记


一、 什么情况下会使用 pprof 分析?

1. 监控告警 / 异常指标(最常见、最重要)

线上或测试环境会有监控(如 Prometheus + Grafana),当出现以下异常时,首选 pprof 进行排查:

  • CPU 飙高:使用率突然升高或持续居高不下。
  • 内存泄漏:内存持续上涨,GC 后也不回落。
  • Goroutine 泄漏:协程数量只增不减。
  • 延迟抖动:接口延迟(P99)明显变差,或某个接口 QPS 上来后响应变慢。
  • GC 压力大:GC 频繁或 STW (Stop-The-World) pause 时间变长。

2. 压力测试 / 性能测试阶段

在测试环境做压测时,故意把并发打高,观察资源占用和延迟。如果检测到 CPU 或内存明显上升,立即使用 pprof 抓取现场进行分析。

3. 本地开发过程中

写功能时自己在本地运行,发现以下苗头:

  • 程序跑一会儿内存就涨很多。
  • 某个函数执行特别慢。
  • 启动后 goroutine 数量异常。
  • 本地 top / htop 里该进程占用明显偏高。
    此时会临时引入 pprof 或用 runtime/pprof 采样分析。

二、 实战演练:基于 go-pprof-practice 的排查

环境:Windows 11 + Go 1.23.6 + Graphviz
目标项目:https://github.com/wolfogre/go-pprof-practice.git

这个项目故意埋了多个性能问题(CPU 高、内存泄漏、协程泄漏、锁竞争等)。我们将通过对该程序进行排查,熟练掌握 pprof 提示内容和具体参数指标的含义。

1. 启动程序与开启 pprof

克隆该仓库并查看代码。可以看到,只需导入 _ "net/http/pprof",并在启动时运行一个 HTTP Server 即可:

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof" // 匿名导入,自动注册 pprof 路由
    "runtime"
)

func init() {
    // 启用 mutex 性能分析(本文暂不展开,仅作完整配置展示)
    runtime.SetMutexProfileFraction(1)
    // 启用 block 性能分析
    runtime.SetBlockProfileRate(1)
}

func main() {
    go func() {
        // 启动 http server,pprof 的一系列 handler 会挂载在该端口下
        if err := http.ListenAndServe(":6060", nil); err != nil {
            log.Fatal(err)
        }
    }()
    
    // ... 业务代码 ...
}

执行 go run main.go 启动该程序。

2. 访问 pprof Web 首页

使用浏览器打开 http://localhost:6060/debug/pprof/,可以看到一个简单的提示页面。它列出了当前可用的 Profile 类型:

  • profile: CPU 分析。默认采集 30 秒。
  • heap: 内存分析(重点)。查看当前正在使用的内存分配,排查内存泄漏的首选。
  • allocs: 内存分析。查看自程序启动以来的所有历史内存分配,排查 GC 压力的首选。
  • goroutine: 当前所有协程的堆栈快照。
  • (其他如 block, mutex, threadcreate 等暂不展开)

三、 pprof 的两种分析方式

获取到 profile 数据后,有两种主流的分析方式:

方式一:Web UI 可视化分析(推荐新手)

直接在浏览器中访问,或在命令行使用 -http 参数启动本地 Web 服务:

go tool pprof -http=:8082 http://localhost:6060/debug/pprof/profile?seconds=30

(前提:本地需安装 Graphviz,否则无法生成火焰图和调用图)
Web 界面直观展示了 Top 函数、火焰图 (Flame Graph) 和调用关系图 (Call Graph),与命令行数据完全一致。
cpu使用详情

方式二:命令行交互式分析

将 profile 文件下载到本地(例如命名为 profile),然后在终端中运行:

go tool pprof .\profile

进入交互模式后,常用命令如下:

  • top / top 20:查看最消耗资源的 Top 10 或 Top 20 函数。
  • list <函数名>:查看指定函数的每一行代码的资源消耗情况。
  • web:生成并打开全局调用关系图(需 Graphviz)。
  • web <函数名>:只生成包含该函数及其调用链的局部图。
  • quit 或 exit:退出交互模式。

四、 CPU 分析实战

在首页点击 profile,浏览器会默认采集 30 秒的 CPU 数据并自动下载文件。使用 go tool pprof 打开后,输入 top 命令,输出如下:

File: main.exe
Type: cpu
Time: Sep 4, 2026 at 4:20pm (CST)
Duration: 30.11s, Total samples = 26.53s (88.12%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 26.50s, 99.89% of 26.53s total
Dropped 6 nodes (cum <= 0.13s)
      flat  flat%   sum%        cum   cum%
    26.50s 99.89% 99.89%     26.50s 99.89%  github.com/.../tiger.(*Tiger).Eat
         0     0% 99.89%     26.50s 99.89%  github.com/.../tiger.(*Tiger).Live
         0     0% 99.89%     26.50s 99.89%  main.main

核心指标解读(打工人比喻)

假设 cum(累计时间) 是老板交给你的一个任务的总耗时(包括你自己干活的时间 + 你委托别人干活的时间)。
而 flat(平铺时间/自身时间) 是你自己亲自敲键盘干活的时间(不包括等别人返回结果的时间)。

  • Total samples = 26.53s:在这 30s 的采样中,程序实际占用 CPU 的总时间是 26.53 秒。剩下的时间程序可能在睡眠、等待 I/O 或空闲。
  • flat:函数自身执行的耗时,不包含它调用的子函数。
  • flat%:flat 占总采样时间 (26.53s) 的百分比。
  • sum%:从第一行到当前行,flat% 的累加值。
  • cum:累计耗时。包含函数自身耗时 + 它调用的所有子函数的耗时。
  • Dropped 6 nodes:为了保持界面简洁,pprof 自动隐藏了 6 个耗时极短的函数(累计耗时 <= 0.13s,约占总时间 0.5%,这是默认过滤阈值)。

通过 list Eat 命令,我们可以直接看到是 tiger.go 第 24 行的空转 for 循环占用了 100% 的 CPU 时间,精准定位问题。


五、 Memory 分析实战

排查内存问题时,我们通常访问 http://localhost:6060/debug/pprof/heap。
(注:allocs 格式与 heap 完全相同,区别在于 heap 看当前存活,allocs 看历史累计)

1. 头部参数分析

heap profile: 2: 805306368 [11: 1089470464] @ heap/1048576

格式为:当前对象数: 当前占用内存 [历史累计对象数: 历史累计内存] @ 采样率(单位:字节)。

  • 2:当前活跃(未回收)的对象数量(即 2 个对象)。
  • 805306368:当前活跃的内存字节数(约 0.75 GB)。
  • [11]:程序运行以来累计分配的对象总数(即 11 个对象)。
  • [1089470464]:程序运行以来累计分配的内存总量(约 1.01 GB)。
  • heap/1048576:采样率,表示每分配 1MB (1048576 字节) 采样一次。

避坑提示:如果“历史累计内存”远远大于“当前内存”,说明程序存在频繁的内存分配和回收,应重点关注 GC 压力(此时应切换查看 allocs)。如果“当前内存”持续增大且不回落,则高度疑似 内存泄漏。

2. 调用栈条目分析

0: 0 [1: 4194304] @ 0xda2cac 0xda24af 0xda2e04 0xb4d13d
#   0xda2cab  github.com/.../mouse.(*Mouse).Steal+0x10b  .../mouse.go:60
#   0xda24ae  github.com/.../mouse.(*Mouse).Live+0x4e    .../mouse.go:25
#   0xda2e03  main.main+0xa3                             .../main.go:41

格式为:inuse_objects: inuse_space [alloc_objects: alloc_space] @ <调用栈地址>

  • 0: 0:这个调用栈当前存活的对象数为 0,占用空间为 0。
  • [1: 4194304]:这个调用栈历史累计分配了 1 个对象,共计 4194304 字节(正好 4MB)。
  • # 开头:符号解析后的人类可读调用栈(由内到外)。

3. 实战项目中的三种典型内存场景

在 go-pprof-practice 项目中,通过观察 heap 输出,我们可以归纳出三种经典场景:

场景 ①:快速大内存分配 + 持有(典型内存泄漏)

1: 1258291200 [1: 1258291200] @ ... (*Mouse).Steal
  • 现象:inuse 和 alloc 数值一致且巨大(如 1.20 GB)。
  • 原因:代码中存在 slice 不断 append 导致 capacity 翻倍增长,且最终被长生命周期对象(如全局变量或长驻协程)持有,GC 无法回收。

场景 ②:慢速内存泄漏

1: 8388608 [100: 838860800] @ ... (*Mouse).Pee.func1
  • 现象:后台存在一个 goroutine,缓慢地(通常带有 time.Sleep)往某个缓冲区里追加内存。单次看很小,但长时间运行后 inuse_space 会持续线性增长。

场景 ③:高频临时分配(GC 压力大,但无泄漏)

0: 0 [46: 771751936] @ ... (*Dog).Run
  • 现象:inuse 为 0: 0,但 [alloc] 数值很大(如累计分配 736 MB)。
  • 原因:Dog.Run 会在循环里反复创建大对象,但这些对象生命周期极短,已经被 GC 成功回收。虽然没有内存泄漏,但频繁的分配和回收会显著增加 GC 的 CPU 负担,优化方向是对象复用(如 sync.Pool)。

六、 参考链接

  1. 小徐先生的编程世界 - Go pprof 实战文章
  2. A Practical Guide to Profiling in Go (JetBrains)

文章作者: ZhangZhengYu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ZhangZhengYu !
评论
 本篇
告别 CPU 飙高与内存泄漏:我的 pprof 实战排查与避坑笔记 告别 CPU 飙高与内存泄漏:我的 pprof 实战排查与避坑笔记
记录我学习 pprof 的心路历程,涵盖 CPU 与内存排查的具体套路、实战中踩过的坑,以及那些文档里没写清楚的调优细节。
下一篇 
理发店的故事:彻底搞懂用户状态管理(Cookie/Session/Token/JWT) 理发店的故事:彻底搞懂用户状态管理(Cookie/Session/Token/JWT)
通过生动的理发店办卡故事,彻底搞懂HTTP无状态、Cookie、Session、Token、JWT的区别与演进。
  目录