目录

实操使用 go pprof 对生产环境进行性能分析(问题定位及代码优化)

简介

最近服务器有个小功能 go 进程 内存占用突然变得很高,正好使用 go pprof 实操进行性能分析排查解决

/img/go-pprof-record-1/0001.png
gopprof排查性能

这是个极小的服务,但是占用内存超过了 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

如下图

/img/go-pprof-record-1/0201.png
go内存堆栈分析火焰图

从图中可以看到:又红又粗的函数有:

  • webdav(*memFile) Write

    通过箭头查看上下的调用链接,可以发现 swaggerFiles init 调用了此函数,通过搜索查看代码,分析服务中确实是使用了 gin-swagger,但是这个项目其实没必要

解决:注释掉这块代码

  • embedFS

        同样通过箭头查看上下的调用链接,发现有个 base64Captcha 调用,这是因为服务使用了一个第三方的验证码生成的包 github.com/mojocn/base64Captcha

embedFS 很明显是加载了文件,通过查看代码发现确实是加载了 fonts 字体

/img/go-pprof-record-1/0202.png
pprof内存堆栈优化分析

加载的字体大概有 5.9MB

解决:替换包,或者直接去掉验证码,我这里就选择一些比较轻量级的包做了替换

(二)总结

从火焰图上看,总体的内存堆栈不算太高,不过通过排查可以看到存在一些项目不需要的,但是有占用内存的函数/包,还是可以优化减少内存的占用,

通过以上的代码优化之后,重启并且观察一段时间后,内存确实稳定下降了一些

/img/go-pprof-record-1/0203.png
优化后的内存占用

三、内存分配分析

分析哪些函数一直在分配内存

内存分配比较多比较频繁的函数会导致内存突然飙高,内存分配不同于内存堆栈,内存堆栈是常驻内存,

具体区别请移步文章: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

如下图

/img/go-pprof-record-1/0301.png
内存分配分析

火焰图有点大,只截取了有问题的部分

从图中可以看到:又红又粗的函数有:

  • webdav(*memFile) Write + embedFS

    这里看不出来这两个函数的调用,我们可以切换为 top 视图(左上角菜单 view 中),就可以看到完整文件位置。

    解决:这个在上一步骤【内存堆栈分析】中就已经做了分析处理(注:代码发布后确实已修复)

  • DBUpdateCronTask

    这里很有可能是索引没有生效,导致锁全表了

    解决:通过对 update where 条件进行索引优化即可

  • go.Marshal

    这里用 go 标准库的 json 序列化操作,看起来分配内存也不算太多,不过我们可以优化为第三方包,性能更佳

    解决:全局替换包 encoding/jsongithub.com/json-iterator/go

  • fmt.Sprintf

        通过排查代码,发现 FmtContent 中使用 fmt.Sprintf ,并且做了大量字符串的拼接,

        解决:字符串拼接优化为使用 :strings.Builder

         关于字符串拼接性能请移步文章:Go语言字符串拼接性能对比与最佳实践 - 深度优化指南

(二)总结

从火焰图上看,总体的内存分配不算太高,不过仍然有优化空间

通过以上的代码优化之后,重启并且观察一段时间后,内存成功下降到了 50MB 以内

/img/go-pprof-record-1/0302.png
内存分配优化结果

四、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  # 协程详情

界面展示说明如下:

/img/go-pprof-record-1/0501.png
pprof协程分析

/img/go-pprof-record-1/0502.png
pprof协程分析结果

从上面界面的数据进行分析

  1. 协程总数量为:27(不算多,但是需要看下每个协程的堆栈情况)

  2. 从 debug=2 的信息中查看每个协程的情况,发现其他都是正常的,有 main、 pprof 、cron、业务等启动的协程

    但是存在一个协程【44】:从堆栈上看,这并不是我服务所需的,通过查看这个堆栈定位到代码的地方,发现之前做链路追踪测试时确实导入这个 go.opentelemetry.io

    但是实际没有任何功能作用

    解决:直接注释掉相关的代码和导包

(二)总结

通过界面先查询协程数量是不是太多,过多可能存在泄漏的情况,再通过 debug=2 界面查看每个协程的执行情况,是否非业务所需但是仍旧执行/阻塞,

然后根据堆栈定位代码,然后优化即可

总结

通过启动 pprof 服务,并且通过 go tool pprof 以及 pprof 内部提供的服务接口对 CPU、内存堆栈、内存分配、协程 等指标进行分析,定位问题函数,优化代码

定位问题函数时,优化后可以先本地做单元测试、压力测试、pprof 测试,具体步骤请查看文章:

go 如何进行 Benchmark 基准测试

Golang 性能分析神器 pprof 详解与实践(图文教程)