目录
概要
pprof的作用
使用方式
交互式常用命令
以profile为例,其余的指标也是用一样的命令
Top N
List func
Traces
web func
Base
Debug=[num]
排查内存泄漏
内存逃逸
内存泄漏的方式
如何判断goroutine泄露
概要
一般而言,性能分析可以从三个层次来考虑:应用层、系统层、代码层。
应用层主要是梳理业务方的使用方式,让他们更合理地使用,在满足使用方需求的前提下,减少无意义的调用;系统层关注服务的架构,例如增加一层缓存;代码层则关心函数的执行效率,例如使用效率更高的开方算法等。
Profiling 是指在程序执行过程中,收集能够反映程序执行状态的数据。在软件工程中,性能分析(performance analysis,也称为 profiling),是以收集程序运行时信息为手段研究程序行为的分析方法,是一种动态程序分析的方法。
Go 语言自带的 pprof 库就可以分析程序的运行情况,并且提供可视化的功能。它包含两个相关的库:
- runtime/pprof
对于只跑一次的程序,例如每天只跑一次的离线预处理程序,调用 pprof 包提供的函数,手动开启性能数据采集。 - net/http/pprof
对于在线服务,对于一个 HTTP Server,访问 pprof 提供的 HTTP 接口,获得性能数据。当然,实际上这里底层也是调用的 runtime/pprof 提供的函数,封装成接口对外提供网络访问。
pprof的作用
allocs 和 heap 采样的信息一致,不过前者是所有对象的内存分配,而 heap 则是活跃对象的内存分配。
- 当 CPU 性能分析启用后,Go runtime 会每 10ms 就暂停一下,记录当前运行的 goroutine 的调用堆栈及相关数据。当性能分析数据保存到硬盘后,我们就可以分析代码中的热点了。
- 内存性能分析则是在堆(Heap and alloc)分配的时候,记录一下调用堆栈。默认情况下,是每 1000 次分配,取样一次,这个数值可以改变。栈(Stack)分配 由于会随时释放,因此不会被内存分析所记录。由于内存分析是取样方式,并且也因为其记录的是分配内存,而不是使用内存。因此使用内存性能分析工具来准确判断程序具体的内存使用是比较困难的。heap 主要记录的是当前活着的(没有被gc)的堆中的内存占用,alloc主要记录一段时间内分配的堆中内存(即使被释放也会记录进去)。
- 阻塞(mutex and block)分析是一个很独特的分析,它有点儿类似于 CPU 性能分析,但是它所记录的是 goroutine 等待资源所花的时间。阻塞分析对分析程序并发瓶颈非常有帮助,阻塞性能分析可以显示出什么时候出现了大批的 goroutine 被阻塞了。阻塞性能分析是特殊的分析工具,在排除 CPU 和内存瓶颈前,不应该用它来分析。
使用方式
当服务集成了pprof功能后,我们可以使用浏览器访问它。如http://localhost:6060/debug/pprof/
点击相应的子链接可以将prof文件下载到本地,profile默认会等待30s以便收集数据,其它的不需要等待。
另一种方式是使用工具链直接下载,如 go tool pprof http://localhost:6060/debug/pprof/block
然后我们可以使用以下命令进入交互式对话: go tool pprof [download_file]
交互式常用命令
以profile为例,其余的指标也是用一样的命令
Top N
打印最耗时的N个函数
List func
打印该函数每行代码的耗时情况
Traces
打印采样期间所有的调用栈,每一次采样的时间为10ms,一次采样为一个。以下为一个sample
20ms github.com/yuin/gopher-lua.(*allocator).LNumber2I
github.com/yuin/gopher-lua.(*registry).SetNumber
github.com/yuin/gopher-lua.opArith
github.com/yuin/gopher-lua.mainLoop
github.com/yuin/gopher-lua.(*LState).callR
github.com/yuin/gopher-lua.(*LState).Call
github.com/yuin/gopher-lua.(*LState).PCall
github.com/yuin/gopher-lua.(*LState).CallByParam
git.xiaojukeji.com/geomining/nemo/src/ilua.(*LState).CallByParam
git.xiaojukeji.com/geomining/nemo/src/activity.(*Activity).QueryDetail
git.xiaojukeji.com/geomining/nemo/src/controller/fetchactdetailctr.(*FetchActDetailController).ServeHTTP
net/http.(*ServeMux).ServeHTTP
github.com/rs/cors.(*Cors).Handler.func1
net/http.HandlerFunc.ServeHTTP
net/http.serverHandler.ServeHTTP
net/http.(*conn).serve
web func
聚焦到该函数,打印上下游调用链(图)
Base
使用base能够对比两个profile文件的差别,就像diff命令一样显示出增加和减少的变化
Debug=[num]
在url后面添加debug=[num]的参数,可以打印出额外的信息。
以goroutine为例,Debug=1时的打印信息如下:
32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 0x6d8559 0x6d831b 0x45abe1
# 0x6d8558 main.alloc2.func1+0xf8 /home/ubuntu/heap/leak_demo.go:53
# 0x6d831a main.alloc2+0x2a /home/ubuntu/heap/leak_demo.go:54
解释
1. goroutine profile: total 32023:32023是goroutine的总数量,
2. 32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 …:32015代表当前有32015个goroutine运行这个调用栈,并且停在相同位置,@后面的十六进制,现在用不到这个数据,所以暂不深究了。
3. 下面是当前goroutine的调用栈,列出了函数和所在文件的行数,这个行数对定位很有帮助,如下:
32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 0x6d8559 0x6d831b 0x45abe1
# 0x6d8558 main.alloc2.func1+0xf8 /home/ubuntu/heap/leak_demo.go:53
# 0x6d831a main.alloc2+0x2a /home/ubuntu/heap/leak_demo.go:54
根据上面的提示,就能判断32015个goroutine运行到leak_demo.go的53行:
Debug=2时的打印信息如下:
goroutine 20 [chan send, 2 minutes]:
main.alloc2.func1(0xc42015e060)
/home/ubuntu/heap/leak_demo.go:53 +0xf9 // 这
main.alloc2(0xc42015e060)
/home/ubuntu/heap/leak_demo.go:54 +0x2b
created by main.alloc1
/home/ubuntu/heap/leak_demo.go:42 +0x3f
解释
1. goroutine 20 [chan send, 2 minutes]:20是goroutine id,[]中是当前goroutine的状态,阻塞在写channel,并且阻塞了2分钟,长时间运行的系统,你能看到阻塞时间更长的情况。
2. 同时,也可以看到调用栈,看当前执行停到哪了:leak_demo.go的53行,
排查内存泄漏
内存逃逸
Go 语言通过逃逸分析会将尽可能多的对象分配到栈上,以使程序可以运行地更快。
这里有个小插曲,你可尝试一下将 16 * constant.Mi 修改成一个较小的值,重新编译运行,会发现并不会引起频繁 GC,原因是在 golang 里,对象是使用堆内存还是栈内存,由编译器进行逃逸分析并决定,如果对象不会逃逸,便可在使用栈内存,但总有意外,就是对象的尺寸过大时,便不得不使用堆内存。所以这里设置申请 16 MiB 的内存就是为了避免编译器直接在栈上分配,如果那样得话就不会涉及到 GC 了。
内存泄漏的方式
如果你启动了1个goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是goroutine泄露。
- goroutine本身的栈所占用的空间造成内存泄露,如未关闭已打开的文件,网络句柄等。
- goroutine中的变量所占用的堆内存导致堆内存泄露,这一部分是能通过heap profile体现出来的。
解决goroutine泄露的根本方法是,程序员自己需要知道goroutine产生的个数和何时退出。
如何判断goroutine泄露
判断依据:在节点正常运行的情况下,隔一段时间获取goroutine的数量,如果后面获取的那次,某些goroutine比前一次多,如果多获取几次,是持续增长的,就极有可能是goroutine泄露。