Go性能分析工具:pprof

1. 简介

pprof 是profile(画像)的缩写,是Go中很常用的获取数据、分析数据的工具。pprof有很多优点:可视化,除此之外,go原生,简单方便,很容易上手。

go tool pprof 是对应的命令行指令。它的源数据既可以是一个http地址,也可以是已经获取到的profile文件。使用go tool pprof命令时,既可以采用交互式终端,也可以采用web进行可视化分析,除此之外可以直接将数据生成svg图片,进行静态的分析。

pprof可以分析以下9中数据:
请添加图片描述

这9项对应的内容如下:

Profile项说明详情
allocs内存分配从程序启动开始,分配的全部内存
block阻塞导致同步原语阻塞的堆栈跟踪
cmdline命令行调用当前程序的命令行调用
goroutinegorouting所有当前 goroutine 的堆栈跟踪
heap活动对象的内存分配抽样。您可以指定 gc 参数以在获取堆样本之前运行 GC
mutex互斥锁争用互斥锁持有者的堆栈跟踪
profileCPU分析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/内存占有率最高的接口。

请添加图片描述

名称说明
FlatCPU在此函数上的运行时间/消耗的内存
Flat%CPU在此函数上运行时间/消耗的内存所占用比例
Sum%从第一行到当前行CPU占有率总和
Cum此函数及其子函数运行所占用时间/消耗的内存
Cum%此函数及其子函数运行所占用时间/消耗的内存的比例
Name函数名称

3.2 函数调用图

通过函数调用图可以很直观形象的看出:哪些函数内存消耗较多
请添加图片描述

3.3 火焰图

火焰图很直观形象看出整体的时间/内存的消耗情况,重点分析占重比比较高的部分。
请添加图片描述

3.4 Peek图

没玩明白,略。
请添加图片描述

3.5 Go源码分析图

列出占比比较高的函数接口在go代码中的位置。
请添加图片描述

3.6 汇编代码分析图

超出知识范围,略。

4. 参考

📖 pprof 的使用

📖 源码包中用法

📖 一文搞懂pprof

[外链图片转存中…(img-KJ4qitqL-1649862180770)]

3.6 汇编代码分析图

超出知识范围,略。

4. 参考

📖 pprof 的使用

📖 源码包中用法

📖 一文搞懂pprof

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叨陪鲤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值