最近解决了我们项目中的一个内存泄露问题,事实再次证明pprof是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,不然就算你手里有屠龙刀,也成不了天下第一,本文就是带你用pprof定位内存泄露问题。
关于Go的内存泄露有这么一句话不知道你听过没有:
10次内存泄露,有9次是goroutine泄露。
我所解决的问题,也是goroutine泄露导致的内存泄露,所以这篇文章主要介绍Go程序的goroutine泄露,掌握了如何定位和解决goroutine泄露,就掌握了内存泄露的大部分场景。
本文草稿最初数据都是生产坏境数据,为了防止敏感内容泄露,全部替换成了demo数据,demo的数据比生产环境数据简单多了,更适合入门理解,有助于掌握pprof。
go pprof基本知识
定位goroutine泄露会使用到pprof,pprof是Go的性能工具,在开始介绍内存泄露前,先简单介绍下pprof的基本使用,更详细的使用给大家推荐了资料。
什么是pprof
pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。
基本使用
使用pprof有多种方式,Go已经现成封装好了1个:net/http/pprof
,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务,能够通过浏览器和命令行2种方式获取运行数据。
看个最简单的pprof的例子:
文件:golang_step_by_step/pprof/pprof/demo.go
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func main() {
// 开启pprof,监听请求
ip := "0.0.0.0:6060"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
}
}
提醒:本文所有代码部分可左右滑动
浏览器方式
输入网址ip:port/debug/pprof/
打开pprof主页,从上到下依次是5类profile信息:
- block:goroutine的阻塞信息,本例就截取自一个goroutine阻塞的demo,但block为0,没掌握block的用法
- goroutine:所有goroutine的信息,下面的
full goroutine stack dump
是输出所有goroutine的调用栈,是goroutine的debug=2,后面会详细介绍。 - heap:堆内存的信息
- mutex:锁的信息
- threadcreate:线程信息
这篇文章我们主要关注goroutine和heap,这两个都会打印调用栈信息,goroutine里面还会包含goroutine的数量信息,heap则是内存分配信息,本文用不到的地方就不展示了,最后推荐几篇文章大家去看。
命令行方式
当连接在服务器终端上的时候,是没有浏览器可以使用的,Go提供了命令行的方式,能够获取以上5类信息,这种方式用起来更方便。
使用命令go tool pprof url
可以获取指定的profile文件,此命令会发起http请求,然后下载数据到本地,之后进入交互式模式,就像gdb一样,可以使用命令查看运行信息,以下是5类请求的方式:
# 下载cpu profile,默认从当前开始收集30s的cpu使用情况,需要等待30s
go tool pprof http://localhost:6060/debug/pprof/profile # 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=120 # wait 120s
# 下载heap profile
go tool pprof http://localhost:6060/debug/pprof/heap # heap profile
# 下载goroutine profile
go tool pprof http://localhost:6060/debug/pprof/goroutine # goroutine profile
# 下载block profile
go tool pprof http://localhost:6060/debug/pprof/block # goroutine blocking profile
# 下载mutex profile
go tool pprof http://localhost:6060/debug/pprof/mutex
上面的pprof/demo.go
太简单了,如果去获取内存profile,几乎获取不到什么,换一个Demo进行内存profile的展示:
文件:golang_step_by_step/pprof/heap/demo2.go
// 展示内存增长和pprof,并不是泄露
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"time"
)
// 运行一段时间:fatal error: runtime: out of memory
func main() {
// 开启pprof
go func() {
ip := "0.0.0.0:6060"
if err := http.ListenAndServe(ip, nil); err != nil {
fmt.Printf("start pprof failed on %s\n", ip)
os.Exit(1)
}
}()
tick := time.Tick(time.Second / 100)
var buf []byte
for range tick {
buf = append(buf, make([]byte, 1024*1024)...)
}
}
上面这个demo会不断的申请内存,把它编译运行起来,然后执行:
$ go tool pprof http://localhost:6060/debug/pprof/heap
Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in /home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz //<--- 下载到的内存profile文件
File: demo // 程序名称
Build ID: a9069a125ee9c0df3713b2149ca859e8d4d11d5a
Type: inuse_space
Time: May 16, 2019 at 8:55pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
(pprof)
(pprof) help // 使用help打印所有可用命令
Commands:
callgrind Outputs a graph in callgrind format
comments Output all profile comments
disasm Output assembly listings annotated with samples
dot Outputs a graph in DOT format
eog Visualize graph through eog
evince Visualize graph through evince
gif Outputs a graph image in GIF format
gv Visualize graph through gv
kcachegrind Visualize report in KCachegrind
list Output annotated source for functions matching regexp
pdf Outputs a graph in PDF format
peek Output callers/callees of functions matching regexp
png Outputs a graph image in PNG format
proto Outputs the profile in compressed protobuf format
ps Outputs a graph in PS format
raw Outputs a text representation of the raw profile
svg Outputs a graph in SVG format
tags Outputs all tags in the profile
text Outputs top entries in text form
top Outputs top entries in text form
topproto Outputs top entries in compressed protobuf format
traces Outputs all profile samples in text form
tree Outputs a text rendering of call graph
web Visualize graph through web browser
weblist Display annotated source in a web browser
o/options List options and their current values
quit/exit/^D Exit pprof
....
以上信息我们只关注2个地方:
- 下载得到的文件:
/home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
,这其中包含了程序名demo
,profile类型alloc
已分配的内存,inuse
代表使用中的内存。 help
可以获取帮助,最先会列出支持的命令,想掌握pprof,要多看看,多尝试。
关于命令,本文只会用到3个,我认为也是最常用的:top
、list
、traces
,分别介绍一下。
top
按指标大小列出前10个函数,比如内存是按内存占用多少,CPU是按执行时间多少。
(pprof) top
Showing nodes accounting for 814.62MB, 100% of 814.62MB total
flat flat% sum% cum cum%
814.62MB 100% 100% 814.62MB 100% main.main
0 0% 100% 814.62MB 100% runtime.main
top会列出5个统计数据:
- flat: 本函数占用的内存量。
- flat%: 本函数内存占使用中内存总量的百分比。
- sum%: 前面每一行flat百分比的和,比如第2行虽然的100% 是 100% + 0%。
- cum: 是累计量,加入main函数调用了函数f,函数f占用的内存量,也会记进来。
- cum%: 是累计量占总量的百分比。
list
查看某个函数的代码,以及该函数每行代码的指标信息,如果函数名不明确,会进行模糊匹配,比如list main
会列出main.main
和runtime.main
。
(pprof) list main.main // 精确列出函数
Total: 814.62MB
ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go
814.62MB 814.62MB (flat, cum) 100% of Total
. . 20: }()
. . 21:
. . 22: tick := time.Tick(time.Second / 100)
. . 23