Golang pprof 使用实践
一、前言
- 先克隆此地址的实验代码
- 查看golang pprof 实战介绍
- 查看Go性能分析工具
- 程序启动后会模拟CPU、内存占用高的程序,建议在资源充足的机器上运行
二、实验代码
2.1 main.go代码
package main
import (
"log"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"time"
"github.com/wolfogre/go-pprof-practice/animal"
)
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
log.SetOutput(os.Stdout)
runtime.GOMAXPROCS(1)
runtime.SetMutexProfileFraction(1)
runtime.SetBlockProfileRate(1)
go func() {
if err := http.ListenAndServe(":6060", nil); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
for {
for _, v := range animal.AllAnimals {
v.Live()
}
time.Sleep(time.Second)
}
}
其他部分代码暂时不用看,是作者设置的模拟“逻辑复杂“的问题程序,本次就利用pprof来找出问题。
2.2 运行代码
-
在终端编译并运行此程序:
>go build >./go-pprof-practice或者直接点击IDE的编译并运行。
跑起来后终端会不断打印日志信息,确保你的机器运行一分钟左右还没崩溃即可继续。
-
保持程序运行,打开浏览器访问
http://localhost:6060/debug/pprof/可以看到:上面的各个选项可以看到程序运行时的各种采样数据,它们的含义是:
类型 描述 allocs 内存分配情况的采样信息 blocks 阻塞操作情况的采样信息 cmdline 显示程序启动命令及参数 goroutine 当前所有协程的堆栈信息 heap 堆上内存使用情况的采样信息 mutex 锁争用情况的采样信息 profile CPU 占用情况的采样信息 threadcreate 系统线程创建情况的采样信息 trace 程序运行跟踪信息 实验程序的作者写的”炸弹程序“主要涉及
allocs、blocks、goroutine、heap、mutex、profile方面的隐藏性能问题,本次实验就在这几个方面实际使用下pprof。 -
直接点进浏览器上对应的连接就可以看到对应的采样信息,如
allocs查看下内存分配情况:可读性太差。
因此我们需要借助
go tool pprof命令来排查问题,此命令为go原生自带。
三、利用pprof排查各项问题
3.1 排查CPU占用过高问题
-
运行起程序,查看任务管理器
可以看到,程序占用CPU还是很大的。
-
保持程序运行,在终端执行命令
go tool pprof http://localhost:6060/debug/pprof/profile然后会出现一个交互式终端,如下图:
-
在交互式终端输入
top命令默认显示了CPU占用前10的调用,会对一些耗时较少的调用函数进行过滤。
这里我们看到
/animal/felidae/tiger.(*Tiger).Eat方法占用CPU最高,达到了91.95%。 -
在交互式终端输入
list Eat命令list命令可以根据指定的正则表达式查找代码行,并显示耗时高的代码行,这里发现时间大多花在了Eat()方法的for循环里。这样就定位到了问题所在的代码行! -
在交互式终端输入
web命令web命令可以用web页面可视化分析,图形化显示调用栈信息。实际是产生一个.svg文件,并调用你系统里设置的默认打开.svg的程序来打开它(我的是浏览器)。框越大、小箭头越粗代表耗时越长。这里也可以看出
tiger.(*Tiger).Eat方法耗时最多。注意:需要先安装
graphviz,可以访问 graphviz 官网 来找到适合自己操作系统的安装方法。 -
看到
tiger.(*Tiger).Eat方法的代码:func (t *Tiger) Eat() { log.Println(t.Name(), "eat") loop := 10000000000 for i := 0; i < loop; i++ { // do nothing } }发现里面执行了100亿次无意义的循环,我们将循环注释掉,再编译运行该程序,执行top命令,看到此时各函数的CPU占用时间就正常了:
-
CPU占用过高的问题解决了,在交互式终端输入
exit退出pprof交互式终端。
3.2 排查内存占用过高问题
-
运行起程序,查看任务管理器
可以看到CPU占用已经降下来了,但是内存占用还是非常高,因为我还开了其他后台应用,所以看起来快占满了,但是确实比运行前高了很多。
-
保持程序运行,在终端执行命令
go tool pprof http://localhost:6060/debug/pprof/heap,这次的URL结尾是heap。然后继续用
list、top命令查看内存占用情况发现问题出在
github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Steal方法,它内存占用很高。 -
也可执行
web命令发现问题确实在这里 -
看到
mouse.(*Mouse).Steal方法的代码:func (m *Mouse) Steal() { log.Println(m.Name(), "steal") max := constant.Gi for len(m.buffer)*constant.Mi < max { m.buffer = append(m.buffer, [constant.Mi]byte{}) } }原来是因为for循环不断向buffer追加长度1MB的byte数组,直到buffer达到1GB为止,且一直没有释放内存。将其注释掉可解决内存占用过高的问题。
3.3 排查频繁内存回收问题
频繁的GC对程序性能也有很大的影响。为了获取程序运行过程中 GC 日志,我们需要先退出实验程序,再在重新启动前赋予一个环境变量(设置GODEBUG=gctrace=1),同时为了避免其他日志的干扰,使用 grep 筛选出 GC 日志查看。
-
linux下执行以下指令(windows上用什么命令我暂时没找到):
GODEBUG=gctrace=1 ./go-pprof-practice | grep gc打印信息如下:
可以看到基本每隔3秒gc一次,每次从16MB清理到0MB,说明程序不断地在请求内存并很快又释放。
-
运行炸弹程序几分钟后再执行命令
go tool pprof http://localhost:6060/debug/pprof/allocs但是我的linux虚拟机分配的内存太小,运行了一分钟不到就系统强制杀死进程了。这部分测试我就先不做了。
-
用pprof的
top、list、web命令继续排查。最后问题是出在dog.(*Dog).Run方法,它里面一直在进行无意义的内存申请,申请完后面没再使用引起自动gc,而该函数又被频繁调用,因此就表现为频繁gc。func (d *Dog) Run() { log.Println(d.Name(), "run") _ = make([]byte, 16*constant.Mi) }
3.4 排查协程泄漏问题
虽然 golang 自带内存回收,一般不会发生内存泄露。但协程本身是可能泄露的,或进而可能导致内存泄露。
-
运行炸弹程序
在浏览器页面可以看到起的协程数
-
保持程序运行,在终端执行命令
go tool pprof http://localhost:6060/debug/pprof/goroutine同样地,
top、list、web这个问题看
web页面会比较好发现,问题在于github.com/wolfogre/go-pprof-practice/animal/canidae/wolf.(*Wolf).Drink方法。看到这部分代码:
func (w *Wolf) Drink() { log.Println(w.Name(), "drink") for i := 0; i < 10; i++ { go func() { time.Sleep(30 * time.Second) }() } }Drink方法每次被调用都会起10个协程,每个协程sleep30秒才退出。当Drink方法被调用多次时,就导致起了很多协程,如果起的协程内部逻辑较复杂出现一直阻塞的情况,协程不能退出,内存无法释放,那么就会造成协程泄漏。
3.5 排查锁的争用问题
排查了上面的资源占用问题后,goroutine数也降下去了,但是发现存在1个mutex争用问题:
-
保持程序运行,在终端执行命令
go tool pprof http://localhost:6060/debug/pprof/mutex同样地,
top、list、web可以看到,问题在于
github.com/wolfogre/go-pprof-practice/animal/canidae/wolf.(*Wolf).Howl方法。 -
wolf.(*Wolf).Howl代码如下func (w *Wolf) Howl() { log.Println(w.Name(), "howl") m := &sync.Mutex{} m.Lock() go func() { time.Sleep(time.Second) m.Unlock() }() m.Lock() }主协程先持有锁,然后启动子协程,主协程在下面的
m.Lock()等待锁,但是子协程sleep了1秒才解锁,主协程才能请求到锁。这不一定是错误,可能是具体业务的需要,但在这里肯定是没必要的。pprof可以帮助我们找到可能的锁争用位置,但是具体是否真的存在问题,还需要自己去辨别。
3.6 排查阻塞操作
除了锁的争用可能导致阻塞之外,还有程序逻辑也可呢个导致阻塞。
-
保持程序运行,在终端执行命令
go tool pprof http://localhost:6060/debug/pprof/mutex同样地,
top、list、web虽然
Live方法也阻塞,但归根结底是由于Live方法调用了Pee方法,实际是阻塞在了Pee。 -
cat.(*Cat).Pee代码如下func (c *Cat) Pee() { log.Println(c.Name(), "pee") <-time.After(time.Second) }该方法里面,要从channel中读数据,但得阻塞1秒后
time.After才会向它的channel中放数据,才能从channel中读到数据。
四、总结
之前在做一个lab时由于涉及到了很多锁和并发,导致很容易出现难以察觉的死锁,当时在网上搜资料时看到别人推荐用pprof分析定位阻塞。那次是我第一次接触pprof。此次借助青训营的机会比较全面地了解了pprof这一工具的强大之处,以后在学习工作中可以多多利用来debug或优化程序。