1. 简介
pprof 是profile(画像)的缩写,是Go中很常用的获取数据、分析数据的工具。pprof有很多优点:可视化,除此之外,go原生,简单方便,很容易上手。
go tool pprof
是对应的命令行指令。它的源数据既可以是一个http地址,也可以是已经获取到的profile文件。使用go tool pprof
命令时,既可以采用交互式终端,也可以采用web进行可视化分析,除此之外可以直接将数据生成svg图片,进行静态的分析。
pprof可以分析以下9中数据:
这9项对应的内容如下:
Profile项 | 说明 | 详情 |
---|---|---|
allocs | 内存分配 | 从程序启动开始,分配的全部内存 |
block | 阻塞 | 导致同步原语阻塞的堆栈跟踪 |
cmdline | 命令行调用 | 当前程序的命令行调用 |
goroutine | gorouting | 所有当前 goroutine 的堆栈跟踪 |
heap | 堆 | 活动对象的内存分配抽样。您可以指定 gc 参数以在获取堆样本之前运行 GC |
mutex | 互斥锁 | 争用互斥锁持有者的堆栈跟踪 |
profile | CPU分析 | CPU 使用率分析。可以在url中,通过seconds指定持续时间(默认30s)。获取配置文件后,使用 go tool pprof 命令分析CPU使用情况 |
threadcreate | 线程创建 | 导致创建新操作系统线程的堆栈跟踪 |
trace | 追踪 | 当前程序的执行轨迹。可以在url中,通过seconds指定持续时间(默认30s)。获取跟踪文件后,使用 go tool trace 命令调查跟踪 |
在性能分析时,使用最多有三种:内存分析(allocs,heap),CPU分析(profile), 阻塞分析(block), 互斥锁分析(mutex)。
下面详细介绍pprof的使用方法。
2. 数据获取
pprof的应用场景主要分为两种:
- 服务型应用,例如web服务器等各种服务类型端的性能分析
- 工具型应用,例如一些命令行工具,执行完毕后直接退出的应用
针对这两种不同的应用场景,pprof有不同的用法。下面做一个详细的介绍
2.1 工具型应用
🏆工具型应用使用runtime/pprof库,将CPU、内存信息手动写到文件中!!!
package main
import (
"fmt"
"os"
"runtime/pprof"
)
func main() {
cpuProfile, err := os.Create("./pprof/cpu_profile")
if err != nil {
fmt.Printf("创建文件失败:%s", err.Error())
return
}
defer cpuProfile.Close()
memProfile, err := os.Create("./pprof/mem_profile")
if err != nil {
fmt.Printf("创建文件失败:%s", err.Error())
return
}
defer memProfile.Close()
//采集CPU信息
pprof.StartCPUProfile(cpuProfile)
defer pprof.StopCPUProfile()
//采集内存信息
pprof.WriteHeapProfile(memProfile)
for i := 0; i < 100; i++ {
fmt.Println("pprof 工具型测试")
}
}
2.2 服务型应用
服务型应用使用net/http/pprof库。
服务型应用使用pprof工具时,首先需要将pprof提供路由注册到当前服务中。由于go的http/https服务框架,允许用户使用原生的http框架,也可以使用其他的框架(例如Gin框架);不同的http服务框架,pprof在处理上略有不同:关键在于是否使用默认的SeverMux结构;下图分别为go中原生net/http框架和Gin框架
2.2.1 使用Go原生http服务框架
当采用go原生的http服务框架,使用pprof非常的简单:只需要导入net/http/pprof包即可。代码如下:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func HelloWorld(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/", HelloWorld)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println(err)
}
}
启动服务后,直接通过浏览器访问pprof路径即可获取采集的信息:
虽然可以通过web查看每一项信息,但是很不友好。我并不喜欢直接使用web查看pprof采用的数据方式,而是采用先通过go tool pprof将数据采集下来,然后再通过go tool pprof开启一个http服务进行,最后通过web访问这个服务,这个时候你的选项非常多,各种图像化输出非常的友好。
go tool pprof http://192.168.1.27:8080/debug/pprof/allocs
go tool pprof http://192.168.1.27:8080/debug/pprof/block
go tool pprof http://192.168.1.27:8080/debug/pprof/cmdline
go tool pprof http://192.168.1.27:8080/debug/pprof/heap
go tool pprof http://192.168.1.27:8080/debug/pprof/mutex
go tool pprof http://192.168.1.27:8080/debug/pprof/profile
go tool pprof http://192.168.1.27:8080/debug/pprof/threadcreate
go tool pprof http://192.168.1.27:8080/debug/pprof/trace
上面的命令,我全部列出来了。不过每次只需要选择需要分析的项进行数据采集即可。命令执行完毕后,会生成对应的pb.gz的文件。之后再使用go tool pprof工具开启一个服务便大功告成。
$ go tool pprof http://192.168.1.27:8080/debug/pprof/allocs
Fetching profile over HTTP from http://192.168.1.27:8080/debug/pprof/allocs
Saved profile in /home/toney/pprof/pprof.server.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
开启http服务,通过浏览器图形化分析采集的数据:
$ go tool pprof -http=192.168.1.27:8081 pprof.server.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
此命令执行后,如果本设备有浏览器,会自动跳转到浏览器中;如果没有浏览器,可以通过http协议远程访问。
除此之外,可以通过左上角的“VIEW”菜单栏,选择不同的视图,其中比较逼格很高的是:火焰图(Flame Graph),它真是一个好东西…
在原生http框架中的用法已经说完了,下面简单介绍下:为何只需要导入net/http/pprof包即可完成这么牛逼的功能?
直接追踪net/http/pprof包,就会发现:
/net/http/pprof包中有一个init()函数。此函数中注册了5个路由,用来获取不同的profile信息
func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}
由于init()函数在导入包的过程中会自动执行。由于我们采用原生的http框架时(路由注册到DefaultServeMux中),http.HandleFunc函数会将路由注册到默认的DefaultServeMux中,此时pprof路径便可以生效,无需再做任何其他操作。
由此联想到其他的web框架,如Gin框架,由于不采用默认的DefaultServerMux结构,因此无法通过直接导入pprof包完成pprof路由的注册。
对于net/http包的web框架,gin框架不熟的可以看看上面两个大图。
2.2.2 使用Gin框架
Gin框架如果要添加pprof, 可以借助github.com/gin-contrib/pprof包
Gin中pprof的使用方式如下:
package main
import (
"fmt"
"net/http"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
)
func helloGin(c *gin.Context) {
fmt.Println("hello Gin")
c.String(http.StatusOK, "欢迎来到三体世界")
}
func GinMain(r *gin.RouterGroup) {
ginGroup := r.Group("/gin") //gin框架前缀
{
ginGroup.GET("/", helloGin)
}
}
func main() {
r := gin.Default()
pprof.Register(r)
web := r.Group("/golang/web") //公共前缀
GinMain(web)
r.Run(":8080")
}
启动程序后,可以看出已经成功注册pprof的相关路由。
之后的步骤就是借助go tool pprof工具采集信息,分析数据。
虽然Gin框架中使用的是"github.com/gin-contrib/pprof",实际上这个包就是在net/http/pprof基础上做的gin封装(将func(w http.ResponseWriter, r *http.Request)格式函数转换为gin.HandlerFunc),方便gin框架调用而已。
// Register the standard HandlerFuncs from the net/http/pprof package with
// the provided gin.Engine. prefixOptions is a optional. If not prefixOptions,
// the default path prefix is used, otherwise first prefixOptions will be path prefix.
func Register(r *gin.Engine, prefixOptions ...string) {
RouteRegister(&(r.RouterGroup), prefixOptions...)
}
// RouteRegister the standard HandlerFuncs from the net/http/pprof package with
// the provided gin.GrouterGroup. prefixOptions is a optional. If not prefixOptions,
// the default path prefix is used, otherwise first prefixOptions will be path prefix.
func RouteRegister(rg *gin.RouterGroup, prefixOptions ...string) {
prefix := getPrefix(prefixOptions...)
prefixRouter := rg.Group(prefix)
{
prefixRouter.GET("/", pprofHandler(pprof.Index))
prefixRouter.GET("/cmdline", pprofHandler(pprof.Cmdline))
prefixRouter.GET("/profile", pprofHandler(pprof.Profile))
prefixRouter.POST("/symbol", pprofHandler(pprof.Symbol))
prefixRouter.GET("/symbol", pprofHandler(pprof.Symbol))
prefixRouter.GET("/trace", pprofHandler(pprof.Trace))
prefixRouter.GET("/allocs", pprofHandler(pprof.Handler("allocs").ServeHTTP))
prefixRouter.GET("/block", pprofHandler(pprof.Handler("block").ServeHTTP))
prefixRouter.GET("/goroutine", pprofHandler(pprof.Handler("goroutine").ServeHTTP))
prefixRouter.GET("/heap", pprofHandler(pprof.Handler("heap").ServeHTTP))
prefixRouter.GET("/mutex", pprofHandler(pprof.Handler("mutex").ServeHTTP))
prefixRouter.GET("/threadcreate", pprofHandler(pprof.Handler("threadcreate").ServeHTTP))
}
}
func pprofHandler(h http.HandlerFunc) gin.HandlerFunc {
handler := http.HandlerFunc(h)
return func(c *gin.Context) {
handler.ServeHTTP(c.Writer, c.Request)
}
}
2.2.3 Grpc类服务
单独开启一个协程用来采集数据信息即可。
3. 数据分析
以内存分配(allocs)分配的内存为例学习go tool pprof数据分析
3.1 TOP图
用来查看CPU/内存占有率最高的接口。
名称 | 说明 |
---|---|
Flat | CPU在此函数上的运行时间/消耗的内存 |
Flat% | CPU在此函数上运行时间/消耗的内存所占用比例 |
Sum% | 从第一行到当前行CPU占有率总和 |
Cum | 此函数及其子函数运行所占用时间/消耗的内存 |
Cum% | 此函数及其子函数运行所占用时间/消耗的内存的比例 |
Name | 函数名称 |
3.2 函数调用图
通过函数调用图可以很直观形象的看出:哪些函数内存消耗较多。
3.3 火焰图
火焰图很直观形象看出整体的时间/内存的消耗情况,重点分析占重比比较高的部分。
3.4 Peek图
没玩明白,略。
3.5 Go源码分析图
列出占比比较高的函数接口在go代码中的位置。
3.6 汇编代码分析图
超出知识范围,略。
4. 参考
[外链图片转存中…(img-KJ4qitqL-1649862180770)]
3.6 汇编代码分析图
超出知识范围,略。