pprof是Golang中一个强大的性能分析工具,可以用来分析CPU、内存的使用情况、阻塞情况、Goroutine的堆栈信息以及锁的争用情况等性能问题。Go语言内部就内置了pprof的采样工具,主要在runtime/pprof
和net/http/pprof
两个包中。pprof工具可通过网页或可视化终端进行分析,展示方式有Top图、调用图、火焰图、Peek、源码和反汇编等。
本文所展示实例来源于go-pprof-practice,一个故意设置"炸弹"来提供pprof锻炼的Go程序,它基本覆盖了常见的性能问题。(注意:为了运行该程序,请保证那你的机器至少有2核CPU和2G内存。)
运行程序,在浏览器中进入http://localhost:6060/debug/pprof/就可以观察到如下图所示:
从上到下依次是内存分配情况、阻塞操作情况、程序启动的命令及参数、所有Goroutine堆栈信息、堆上内存使用情况、互斥锁的争用情况、CPU占比情况、线程创建情况和程序运行的跟踪信息。每一个点开可以看到详细情况(其中profile和strace会下载文件而不是在浏览器打开),但不建议这么做,因为很多,很难去看到自己想要的东西。
直接阅读这些信息很不直观,我们可以通过go tool pprof
命令来进一步得到更好的信息,这个命令go自带,无需安装。接下来我们直接开始正式分析。
CPU占用过高
可以观察到程序的CPU占用很高,接下来我们使用go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
来获取相关信息,其中的second
参数表示取样时间,此处设置为10s。可以看到我们进入了一个交互界面。
接下来我们输入top
即可查看占用CPU最高的几个调用。
其中每一列的的意思分别为:
列项 | 意思 |
---|---|
flat | 当前函数本身执行耗时 |
flat% | flat占CPU总时间的比例 |
sum% | 上面每一行的flat%总和 |
cum | 当前函数本身加上其调用函数的总耗时 |
cum% | cum占CPU总时间的比例 |
显然,CPU占用过高是由github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat
造成的,接下来可用list
来查找指定代码行,输入list Eat
,查看代码具体问题:
可以观察到第24行有一个100亿次的空循环占用了大量的CPU时间,成功定位到问题。
在你的机器上安装graphviz后,通过输入web
就可以看到图形化的调用栈信息:
图中可以很明显的看到tiger.(*Tiger).Eat
特别大,表明它的CPU占用比较高。
通过输入exit
或q
离开交互界面,注释掉该处代码,本小节结束。
内存占用过高
重新编译该程序,再次运行,可以发现,CPU占用已经降下来,但内存占用依旧很高。
使用命令go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
可以在8080端口打开一个web网页,页面如下:
通过该图可以发现mouse.(*Mouse).Steal
占用了大量内存。
点击左上角的view,结果如下:
通过点击可以查看各种不同的视图,非常有用。此处不再展示各种视图,读者可自行查看。
点击其右边的sample,结果如下:
自上到下他们分别表示:程序累计申请的对象数、程序累计申请的内存大小、程序当前持有的对象数、程序当前占有的内存大小。同样本文不再展示。
通过source视图查看对应的代码,注释掉即可。
之后再次查看alloc_space,可以发现dog.(*Dog).Run
分配了很多内存:
说明它一直在进行无意义的内存申请,申请完就被回收掉了。同样的方法定位到代码,注释掉即可。
协程泄露排查
虽然golang带有内存回收,一般不会发生内存泄露,但是是存在泄露的可能得。
在浏览器中可以看到,此时已经有一百多条协程。
这看上去似乎不是很正常,我们通过pprof排查一下。使用go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
命令打开浏览器,发现wolf.(*Wolf).Drink
创建了100个协程。
还是老样子,在source视图下定位问题,然后注释掉。注意直接找可能不太好找,可以直接在搜索栏搜索。
注释掉之后,发现协程数恢复正常水平。
锁的争用
接下来排查锁的争用问题,老样子,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
。可以发现wolf.(*Wolf).Howl
的锁竞争问题严重。
同样的方式定位到错误。
可以发现,这个锁由主协程 Lock,并启动子协程去 Unlock,主协程会阻塞在第二次 Lock 这儿等待子协程完成任务,但由于子协程足足睡眠了一秒,导致主协程等待这个锁释放足足等了一秒钟。
虽然在具体的业务逻辑中可能存在这种情况,但在此处我们认为它是一个错误,注释掉它。
阻塞操作
接下来是最后一个问题,阻塞。
老样子,使用命令go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
查看阻塞情况。
可以发现,cat.(*Cat).Pee
是发生了阻塞。
找到源码,发现是从一个 channel 里读数据时,发生了阻塞,直到这个 channel 在一秒后才有数据读出,这就导致程序阻塞了一秒而非睡眠了一秒。
注意到浏览器中显示有两个block,而我们只发现了一个block,在命令行可以发现另一个block,不展示的原因在于,在展示之前会设置一个阈值,只有高于这个阈值才会显示,所以过滤掉了另一个。这种设置使得我们在更复杂的程序中更方便定位问题,其实是有利的。
注释掉它,成功排查完成,恭喜你,成功解决了所有问题。
结语
相信通过本文,你已经对pprof有了一个基本的了解,但本文还是太浅显了,仅仅是一个入门而已,旨在消除你对pprof的陌生。希望你能通过进一步的学习熟练掌握pprof。
最后,受作者能力限制,如有错误或不清晰之处,还望体谅。