使用pprof快速定位Go内存泄漏

tech2025-11-04  8

什么是pprof

pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。

代码实现

package main import ( "fmt" "net/http" _ "net/http/pprof" // 引入pprof,调用init方法 ) func main() { // 生产环境应仅在本地监听pprof go func() { ip := "127.0.0.1:9527" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Println("开启pprof失败", ip, err) } }() // 业务代码运行中 http.ListenAndServe("0.0.0.0:8081", nil) }

使用方式

1.浏览器方式

地址:http://127.0.0.1:9527/debug/pprof/

界面:

allocs: 内存分配分析

block:同步阻塞分析

cmdline:命令行调用分析

goroutine:goroutine分析

heap:堆内存分析

mutex:锁竞争分析

profile:30s的CPU使用情况分析

threadcreate: 创建新OS线程的堆栈跟踪

trace:当前程序执行的追溯(比如一个get请求的追溯)

2.命令行方式(适用于服务端调试)

# 下载cpu profile,默认从当前开始收集30s的cpu使用情况,需要等待30s go tool pprof http://localhost:9527/debug/pprof/profile # 30-second CPU profile go tool pprof http://localhost:9527/debug/pprof/profile?seconds=120 # wait 120s # 下载heap profile go tool pprof http://localhost:9527/debug/pprof/heap # heap profile # 下载goroutine profile go tool pprof http://localhost:9527/debug/pprof/goroutine # goroutine profile # 下载block profile go tool pprof http://localhost:9527/debug/pprof/block # goroutine blocking profile # 下载mutex profile go tool pprof http://localhost:9527/debug/pprof/mutex # 略

什么是内存泄漏

内存泄露指的是程序运行过程中已不再使用的内存,没有被释放掉,导致这些内存无法被使用,直到程序结束这些内存才被释放的问题。

在golang中内存泄漏一般来源于

1.goroutine泄漏

2.堆内存泄漏

内存泄漏的内存使用量图一般是这样的:

ps: 如果没有云平台的这种内存监控工具的话就可以自己用pidstat命令crontab一下定时获取进程占用的物理内存做分析。

错误的分析方式

大家第一反应肯定是根据调用路径图,火焰图等等进行追溯定位内存泄漏,这样其实又麻烦又不精确(代码多的时候看起来就是一大坨)

在Dave的high-performance-go-workshop中是如下说的

https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html#types_of_profiles

内存分析只是对堆内存的分析,不包括栈内存(因为栈内存被认识是廉价的,不需要记录,回收起来很方便)内存分析是抽样分析,每1000次分配抽样一次这种,不精确

所以堆内存分析只能发现问题,不容易找出问题的来源。

内存泄漏如何找到问题

错误案例:

package main import ( "fmt" "net/http" _ "net/http/pprof" // 引入pprof,仅使用init方法 "time" ) func main() { // 生产环境应仅在本地监听pprof go func() { ip := "127.0.0.1:9527" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Println("开启pprof失败", ip, err) } }() // 业务代码运行 outCh := make(chan int) // 死代码,永不读取 go func() { if false { <-outCh } select {} }() // 每秒起10个goroutine,goroutine会阻塞,不释放内存 tick := time.Tick(time.Second / 10) i := 0 for range tick { i++ fmt.Println(i) alloc1(outCh) // 不停的有goruntine因为outCh堵塞,无法释放 } } // 一个外层函数 func alloc1(outCh chan<- int) { go alloc2(outCh) } // 一个内层函数 func alloc2(outCh chan<- int) { func() { defer fmt.Println("alloc-fm exit") // 分配内存,假用一下 buf := make([]byte, 1024*1024*10) _ = len(buf) fmt.Println("alloc done") outCh <- 0 // 54行 }() }

直接上结论(打两个点+top+traces一套带走):

1.内存泄漏大概率是goroutine泄漏,且堆内存分析不大靠谱,所以先上goroutine

2.取2个时间点的goruntine

go tool pprof http://localhost:9527/debug/pprof/goroutine # 等一会 go tool pprof http://localhost:9527/debug/pprof/goroutine # 生成文件: # pprof.goroutine.001.pb.gz 和 pprof.goroutine.002.pb.gz

3.对比两个文件

go tool pprof -base pprof.goroutine.001.pb.gz pprof.goroutine.002.pb.gz

4.结合top命令发现问题(top命令能给出***所选分析内容差异最大的***10条内容-此处所选的分析内容为goroutine)

可以看到有121个goroutine处于挂起(runtime.gopark)状态,即goroutine泄漏

5.定位问题trace命令,可以查看栈调用信息,就能很快的找到问题在于main包中alloc2方法的匿名函数出现了channel send堵塞。

PS:同理,堆内存以及其他性能指标都可以用这个方法来查找差异,唯一的区别就在于打点的时候取的指标不同。

# 堆内存对比分析的话就打点堆内存 go tool pprof http://localhost:9527/debug/pprof/heap # 其他同理

当然如果所在的机器上还有源代码的话,可以使用list命令更具体到究竟是哪一行代码的问题

list一下即可找到是54行 outCh <- 0 这一行发生了122个goroutine的阻塞

最新回复(0)