实操使用 go pprof 对生产环境进行性能分析(问题定位及代码优化)
简介
最近服务器有个小功能 go 进程 内存占用突然变得很高,正好使用 go pprof 实操进行性能分析排查解决
这是个极小的服务,但是占用内存超过了 100MB,而且本身服务器内存就比较吃紧,因此尝试使用 pprof 进行性能分析,看看能不能优化到 50 MB 以内
关于 pprof 的使用教程,请移步文章:Golang 性能分析神器 pprof 详解与实践(图文教程)
接下来使用 go pprof 进行实操性能分析排查
一、开启 pprof
在项目启动时开启 pprof
导包:_ “net/http/pprof”
启动:http.ListenAndServe(":30552", nil)
参考代码如下
import _ "net/http/pprof"
func StartPprof() {
go func() {
logs.Info(http.ListenAndServe(":30552", nil))
}()
// 查看哪些函数占用内存比较多
// go tool pprof -http=localhost:8081 -seconds=10 http://0.0.0.0:30552/debug/pprof/heap # 内存堆栈分析
// go tool pprof -http=localhost:8081 -seconds=10 http://0.0.0.0:30552/debug/pprof/allocs # 内存分配分析
// 查看哪些函数占用耗时比较多
// go tool pprof -http=localhost:8081 -seconds=60 http://0.0.0.0:30552/debug/pprof/profile # 耗时分析
// go tool pprof -http=localhost:8081 -seconds=60 http://0.0.0.0:30552/debug/pprof/goroutine # 协程分析
// go tool pprof -http=localhost:8081 -seconds=10 http://0.0.0.0:30552/debug/pprof/block # 阻塞分析
// go tool pprof -http=localhost:8081 -seconds=10 http://0.0.0.0:30552/debug/pprof/mutex # 锁分析
}
func main(){
....
StartPprof()
err = router.Run(address)
if err != nil {
panic(err)
}
....
}
二、内存堆栈分析
分析哪些函数一直在占用内存
(一)问题分析
go tool pprof -http=0.0.0.0:8081 http://0.0.0.0:30552/debug/pprof/heap
这里我没有加 -seconds ,表示采样数据为:进程启动以来的总数据, 具体参数的使用:Golang 性能分析神器 pprof 详解与实践(图文教程)
浏览器会自动打开:0.0.0.0:8081/ui
从图中可以看到:又红又粗的函数有:
- webdav(*memFile) Write
通过箭头查看上下的调用链接,可以发现 swaggerFiles init 调用了此函数,通过搜索查看代码,分析服务中确实是使用了 gin-swagger,但是这个项目其实没必要
解决:注释掉这块代码
- embedFS
同样通过箭头查看上下的调用链接,发现有个 base64Captcha 调用,这是因为服务使用了一个第三方的验证码生成的包 github.com/mojocn/base64Captcha
embedFS
很明显是加载了文件,通过查看代码发现确实是加载了 fonts 字体
加载的字体大概有 5.9MB
解决:替换包,或者直接去掉验证码,我这里就选择一些比较轻量级的包做了替换
(二)总结
从火焰图上看,总体的内存堆栈不算太高,不过通过排查可以看到存在一些项目不需要的,但是有占用内存的函数/包,还是可以优化减少内存的占用,
通过以上的代码优化之后,重启并且观察一段时间后,内存确实稳定下降了一些
三、内存分配分析
分析哪些函数一直在分配内存
内存分配比较多比较频繁的函数会导致内存突然飙高,内存分配不同于内存堆栈,内存堆栈是常驻内存,
具体区别请移步文章:Golang 性能分析神器 pprof 详解与实践(图文教程
(一)问题分析
go tool pprof -http=0.0.0.0:8081 http://0.0.0.0:30552/debug/pprof/allocs
浏览器会自动打开:0.0.0.0:8081/ui
如下图
火焰图有点大,只截取了有问题的部分
从图中可以看到:又红又粗的函数有:
-
webdav(*memFile) Write + embedFS
这里看不出来这两个函数的调用,我们可以切换为
top
视图(左上角菜单 view 中),就可以看到完整文件位置。解决:这个在上一步骤【内存堆栈分析】中就已经做了分析处理(注:代码发布后确实已修复)
-
DBUpdateCronTask
这里很有可能是索引没有生效,导致锁全表了
解决:通过对 update where 条件进行索引优化即可
-
go.Marshal
这里用 go 标准库的 json 序列化操作,看起来分配内存也不算太多,不过我们可以优化为第三方包,性能更佳
解决:全局替换包
encoding/json
为github.com/json-iterator/go
-
fmt.Sprintf
通过排查代码,发现 FmtContent 中使用 fmt.Sprintf ,并且做了大量字符串的拼接,
解决:字符串拼接优化为使用 :strings.Builder
关于字符串拼接性能请移步文章:Go语言字符串拼接性能对比与最佳实践 - 深度优化指南
(二)总结
从火焰图上看,总体的内存分配不算太高,不过仍然有优化空间
通过以上的代码优化之后,重启并且观察一段时间后,内存成功下降到了 50MB 以内
四、CPU 耗时分析
这里从 top 可以看出其实并没有什么 CPU 飙高的情况,不过既然都优化了,就顺便看看哪些函数在占用吧
(一)问题分析
go tool pprof -http=0.0.0.0:8081 http://0.0.0.0:30552/debug/pprof/profile
从火焰图上没看到什么耗时比较异常的函数
(二)总结
从 top 没看到CPU异常,火焰图也没有异常,如果存在异常,排查和优化的方法和前面内存的步骤是一样的
五、协程分析
分析程序开启的协程情况
(一)问题分析
浏览器直接打开以下命令
http://0.0.0.0:30552/debug/pprof/goroutine?debug=1 # 协程总览
http://0.0.0.0:30552/debug/pprof/goroutine?debug=2 # 协程详情
界面展示说明如下:
从上面界面的数据进行分析:
-
协程总数量为:27(不算多,但是需要看下每个协程的堆栈情况)
-
从 debug=2 的信息中查看每个协程的情况,发现其他都是正常的,有 main、 pprof 、cron、业务等启动的协程
但是存在一个协程【44】:从堆栈上看,这并不是我服务所需的,通过查看这个堆栈定位到代码的地方,发现之前做链路追踪测试时确实导入这个
go.opentelemetry.io
包但是实际没有任何功能作用
解决:直接注释掉相关的代码和导包
(二)总结
通过界面先查询协程数量是不是太多,过多可能存在泄漏的情况,再通过 debug=2 界面查看每个协程的执行情况,是否非业务所需但是仍旧执行/阻塞,
然后根据堆栈定位代码,然后优化即可
总结
通过启动 pprof 服务,并且通过 go tool pprof 以及 pprof 内部提供的服务接口对 CPU、内存堆栈、内存分配、协程 等指标进行分析,定位问题函数,优化代码
定位问题函数时,优化后可以先本地做单元测试、压力测试、pprof 测试,具体步骤请查看文章: