Skip to content

Golang性能分析-pprof

遵从代码最佳实践或者代码规范指导,可以避免一些参见的坑,但在运行代码时遇到各种问题后,还是需要深入代码了解代码性能和瓶颈, 以便能够发现问题、解决问题和做出相应的优化。

性能分析器(Profiler)是一种动态的性能分析工具,可以提供程序在运行过程中的各种维度的关键性能分析,可以用来解决各种性能问题,定位内存泄露, 线程竞争死锁等问题。

Go运行时以pprof可视化工具预期的格式提供性能分析数据,分析数据可以在测试期间通过go test,或者从net/http/pprof包提供的endpoint收集。我们需要收集性能分析数据,并通过pprof工具对程序进行分析,并支持可视化数据。

Go 通过runtime/pprof包提对profile提供了内置的支持:

  • CPU: CPU分析采集一定周期内程序对CPU的使用情况,确定程序在主动消耗CPU周期是如何消耗时间的
  • Heap: Heap分析,记录内存分配,用于监视当前和历史内存使用情况,以及检查内存泄露
  • Goroutine: Goroutine分析报告所有当前正在运行goroutine的堆栈跟踪信息
  • threadcreate: 报告程序中导致创建新OS线程的堆栈跟踪信息
  • Block: 阻塞分析,显示goroutine阻塞等待同步(包括定时器通道)的位置,默认没有启用,可以使用 runtime.SetBlockProfileRate 启用它
  • Mutex: 互斥锁分析,报告互斥锁的竞争情况,当认为互斥锁争用导致CPU没有得到充分使用时,可以使用这种分析,默认没有启动,可以使用runtime.SetMutexProfileFraction 启用它

目前有三种方式进行数据采集:

  • go test: 通过运行测试用例继续采集
  • runtime/pprof: 直接在代码中指定采集程序运行数据
  • net/http/pprof: 采集http server运行时数据进行分析

测试标准库在go test中内置了对采集性能分析数据的内置支持,如下面的命令,在当前目录中运行测试,并将CPU和内存采样数据分别写入cpu.prof和 mem.prof:

Terminal window
go test -cpuprofile cpu.prof -memprofile mem.prof .

在长时间运行的http服务中,只需要导入net/http/pprof包,服务便可以进行采集数据,并多出一个终结点(endpoint)/debug/pprof,可用于观察采集到的 性能分析数据。包导入语句如:

_ "net/http/pprof"

net/http/pprof 包初始化时在DefaultServeMux中为给定的URL注册了处理程序

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)
}

这些handler将采样数据写入到http.ResponseWriter,导入net/http/pprof后需要做的,只是启动一个HTTP server就可以了,如:

http.ListenAndServe(":8080", nil)

完整的最小采集demo:

package main
import (
"log"
"net/http"
"net/http/httputil"
_ "net/http/pprof"
)
func main() {
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
c, _ := httputil.DumpRequest(r, true)
log.Println(string(c))
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(201)
rw.Write([]byte("{}"))
})
http.ListenAndServe(":8080", nil)
}

运行程序后,就可以在浏览器访问localhost:8080/debug/pprof实时采样数据总览页面:

pprof

  • allocs: 所有已分配内存的采样
  • block: 导致阻塞同步的堆栈跟踪
  • cmdline: 显示程序启动的路径命令和参数
  • goroutine: 所有当前goroutine的堆栈跟踪
  • heap: 当前活动对象的内存采样
  • mutext: 互斥锁持有者的堆栈跟踪
  • profile: 进行CPU采样,采样完成得到一个供分析用的profile文件,默认采集间隔是30秒
  • threadcreate: 创建新OS线程的堆栈跟踪
  • trace: 程序的执行跟踪信息

直接通过上面的方式查看采样数据枯燥且缺乏直观性,后面介绍可以借助go tool pprof命令,可以更直观方便的分析采样数据。

通过使用runtime/pprof包,可以直接在代码中进行采样,如可以使用pprof.StartCPUProfile(io.Writer)开始CPU采样, 使用pprof.StopCPUProfile() 停止CPU采样,如:

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
func main() {
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer f.Close() // error handling omitted for example
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
// ... rest of the program ...
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer f.Close() // error handling omitted for example
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write memory profile: ", err)
}
}
}

运行程序:

Terminal window
go run -cpuprofile cpu.prof -memprofile mem.prof .

将CPU和内存采样数据分别写入文件cpu.prof 和 mem.prof。

通过go tool pprof命令在交互命令行终端中使用采用数据,对于采样数据文件profile可以使用:

Terminal window
go tool pprof profile

profile

对于通过「采样方式2.2」HTTP 终结点方式,可以使用go tool pprof url的方式,如:

Terminal window
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

profile-2

采样url,在可以在采样总览页面中获取到:

ulr

使用pprof可视化界面时,在go tool pprof命令通过指定端口参数-http,运行一个pprof分析用的站点,如:

  • 查看当前所有goroutine的堆栈跟踪
go tool pprof -http :9090 'http://localhost:8080/debug/pprof/goroutine'

pprof ui

通过pprof提供的可视化界面,可以更清晰直观的查看调用链等信息。

  • Top top

  • Graph graph

  • Flame Graph flame

  • Peek peek

  • Source source

  • Disassemble disassemble