Go 语言自带的 pprof 是一种性能分析工具,用于帮助开发者分析和优化程序的性能,它可以提供关于 CPU 耗时、内存分配、网络 IO、系统调用、阻塞等待等方面的详细信息。
我们来通过一个小例子直观的感受下 pprof 的使用方法,本文所有示例代码运行环境是 go1.20 darwin/arm64。
package main
import (
"bytes"
"log"
"math/rand"
"net/http"
_ "net/http/pprof" // 开启 pprof
"time"
)
// 换成一个随机字符串并返回对应的缓冲区
func genRandomBytes() *bytes.Buffer {
var buff bytes.Buffer
for i := 1; i < 10000; i++ {
buff.Write([]byte{'0' + byte(rand.Intn(10))})
}
return &buff
}
func main() {
go func() {
for {
// 循环调用生成字符串方法,模拟 CPU 负载
for i := 0; i < 1000; i++ {
_ = genRandomBytes()
}
time.Sleep(time.Second)
}
}()
// 程序绑定到 6060 端口
// pprof 结果也必须通过该接口获取
log.Fatal(http.ListenAndServe("127.0.0.1:6060", nil))
}
$ go build main.go && ./main
通过浏览器打开下面的链接,可以看到 pprof 的实时采样数据。
http://127.0.0.1:6060/debug/pprof/
正常情况下,我们可以看到如下页面:
例如我们想要拿到 CPU 的采样数据,可以点击图中的 profile 链接,此时会进入默认的 30 秒采样阶段 (浏览器表现为不停转圈),30 秒之后,浏览器会弹出下载窗口, 提示你是否要保存名称为 profile 的采样文件。
这里我们通过 CURL 的方式来直接下载,读者可以利用等待 CURL 的时间大致了解一下页面上的几个采样类型。
# 下载 CPU 样本数据,并保存到 cpu.pprof 文件中
$ curl -sS "http://127.0.0.1:6060/debug/pprof/profile?seconds=30" -o cpu.pprof
Go 语言自带的命令行工具集中的 go tool pprof
可以对样本数据进行分析。
# 对 cpu.pprof 文件进行分析
$ go tool pprof main cpu.pprof
输入上述命令后,进入 pprof 的专用命令行中:
...
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 等待用户输入
此时我们可以输入 help, 看一下相关的操作提示 (Tips: 我们无需死记硬背命令行参数,使用 pprof 的时候输入 help 寻找对应的参数即可):
# 输入 top 后,命令行输出结果如下
Commands:
...
top Outputs top entries in text form
topproto Outputs top entries in compressed protobuf format
traces Outputs all profile samples in text form
tree Outputs a text rendering of call graph
web Visualize graph through web browser
...
Options:
...
这里以 top 参数为例,输出采样数据 CPU 使用 top 5 的调用:
(pprof) top 5
# 输出结果如下
Showing nodes accounting for 4090ms, 75.74% of 5400ms total
Dropped 56 nodes (cum <= 27ms)
Showing top 5 nodes out of 53
flat flat% sum% cum cum%
1290ms 23.89% 23.89% 2920ms 54.07% math/rand.(*lockedSource).Int63
1260ms 23.33% 47.22% 1260ms 23.33% sync.(*Mutex).Unlock (inline)
740ms 13.70% 60.93% 3710ms 68.70% math/rand.(*Rand).Int31n
490ms 9.07% 70.00% 870ms 16.11% bytes.(*Buffer).Write
310ms 5.74% 75.74% 310ms 5.74% math/rand.(*rngSource).Uint64 (inline)
通过上面的输出结果,可以很清晰地看到消耗最多 CPU 资源的 5 个调用方法。
这里需要注意的是第三列的 sum% 的字段,表示的是前 N 行的 flat% 列的数值总和,例如第三行的的 60.93% 就表示:
第一行 + 第二行 + 第三行 = 23.89% + 23.33% + 13.70% = 60.93%
# 通过 cum 列排序
(pprof) top 5 -cum
...
下面对每列表示的数据做一个简单的概述:
flat 和 cum 的区别在于: flat 只统计函数自身的 CPU 使用时间,不包括调用其他函数的时间,而 cum 则包括了函数自身的时间和调用其他函数的时间。
刚才的示例过程是先使用 CURL 下载采样数据,然后使用 pprof 命令工具进行分析,可以将这两个步骤合并到一个步骤中。
$ go tool pprof "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"
上述命令会在采样结果保存完成后,自动进入 pprof 的专用命令行中。
采样类型 | 描述 |
---|---|
allocs | 内存总分配 |
block | 同步原语导致的阻塞 |
cmdline | 进程启动命令行 |
goroutine | 当前所有的 goroutine |
heap | 内存分析 |
mutex | 互斥锁 |
profile | CPU |
threadcreate | 操作系统中的线程创建 |
trace | 程序执行 trace, 和其他样本数据不同的是,这个需要使用 go tool trace 来分析 |
full goroutine stack dump | 打印所有 goroutine 的堆栈 |
通过浏览器直接访问 http://127.0.0.1:6060/debug/pprof/heap?debug=1, 然后加载页面底部,可以看到内存相关的数据,我们选择一些常用的指标进行说明。
# 示例内存分析数据
# 对应的标准库中的对象为 runtime.MemStats{}
# runtime.MemStats
# Alloc = 1382056
# TotalAlloc = 2855089694704
# Sys = 19334408
# Lookups = 0
# Mallocs = 871957108
# Frees = 871955873
# HeapAlloc = 1382056
# HeapSys = 11993088
# HeapIdle = 9977856
# HeapInuse = 2015232
# HeapReleased = 7012352
# HeapObjects = 1235
# Stack = 589824 / 589824
...
字段名称 | 描述 |
---|---|
TotalAlloc | 进程启动后累计分配总内存 |
Sys | 从操作系统申请的总内存 |
Frees | 累计释放的堆内存 |
HeapAlloc | 当前正在使用的内存 + 未被 GC 释放的内存 |
HeapSys | 从操作系统申请的堆内存 |
HeapIdle | 空闲内存,可以被申请甚至作为栈使用 |
HeapInuse | 当前正在使用的堆内存 |
HeapReleased | 已经归还给操作系统的堆内存 |
Sys | 从操作系统申请的总内存 |
StackSys | StackInuse + 直接从操作系统申请的内存 |
StackInuse | 当前正在使用的栈内存 |
排查内存相关问题时,除了关注内存使用和闲置相关参数外,还要关注 GC 频率、GC 时间等参数。
读者可以在 pprof 页面上点击不同的链接来查看不同的采样数据,需要注意的是: profile 和 trace 两个指标默认的采样时间是 30 秒,其他指标都是即时的 (每次刷新显示不同的数据)。
pprof 可以生成样本数据分析图 (如 CPU 火焰图) 达到更好的可观测性,但是生成的图需要使用 graphiv 进行渲染,如果要运行 pprof 的机器还未安装 graphviz, 可以参考 官网的安装教程[1] 进行安装。
虽然 pprof 提供的命令行操作方式已经非常方便,但是如果我们希望使用更加直观的 UI 方式来展现采样分析结果,可以使用 Web UI 功能。Web UI 提供了和 pprof 命令行同样的功能,但是可观测性更强。
$ go tool pprof -http=127.0.0.1:6061 "http://127.0.0.1:6060/debug/pprof/profile?seconds=30"
# 输出如下
Fetching profile over HTTP from http://127.0.0.1:6060/debug/pprof/profile?seconds=30
Saved profile in /home/stars/pprof/pprof.main.samples.cpu.004.pb.gz
Serving web UI on http://127.0.0.1:6061
...
上述命令会在采样结果保存完成后,新启动一个 Web Server (端口为 6061),自动打开浏览器并跳转到 http://127.0.0.1:6061/ui/。
输入 go tool pprof main cpu.pprof 进入到 pprof 的专用命令行中,然后输入参数 web。
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) web
# 输出如下
...
+ Start "/tmp/pprof001.svg"
...
上述命令会生成一个 SVG 文件,然后自动在浏览器打开,但是打开的页面中只有一个图片,没有其他功能 (所以后文中都采用刚才的第一种方法作为 Web UI 打开方式)。
针对单个采样结果的分析,Web UI 提供了非常强大的功能,下面通过示例图来演示。
所有的功能菜单都在导航栏,直接选择对应的数据即可。
功能和 pprof 命令行中的 top 一样,显示采样 top N 的数据:
基于 Brendan Gregg 大佬发明的火焰图,根据图中的颜色深浅,调用链形状的长短,几乎可以瞬间定位到出现问题的方法。
功能和 pprof 命令行中的 top 一样,显示采样 top N 的数据:
可以看到采样分析数据和对应的代码。
可以看到采样分析数据和对应的代码函数相关信息。
限于篇幅,其他菜单功能这里就不一一展示了,读者可以在 Web UI 页面自行研究。
写到这里,pprof 数据采样分析差不多就介绍完了,然后这里再简单介绍一下 trace 数据采样分析,两者的区别在于:
和基于毫秒频率的 pprof 定时数据采样相比,基于事件的 trace 精确到了纳秒,因此对系统的性能影响还是非常大的。 一般情况下不需要使用 trace 来定位性能问题,除非是需要获取运行时的程序数据,例如 goroutine 调度、阻塞、GC 等。
可以直接将路径中的 path 参数指定为 trace, 即可开始 trace 采样。
# trace 采样数据,时间 30 秒
$ curl -sS "http://127.0.0.1:6060/debug/pprof/trace?seconds=30" -o trace.out
采样数据完成后,可以通过 go tool trace 命令打开采样文件:
$ go tool trace trace.out
# 输入如下:
2023/07/29 11:44:53 Parsing trace...
2023/07/29 11:44:53 Splitting trace...
2023/07/29 11:44:53 Opening browser. Trace viewer is listening on http://127.0.0.1:64552
输入上述命令之后,会启动一个 Web Server (端口号随机), 并且自动在浏览器中打开页面:
trace Web UI 和 pprof Web UI 提供的功能类似,只不过各功能点的链接分散在了页面中,而不是集中到顶部的导航区域。
点击页面的 View trace 链接,可以显示 goroutine、内存分析、操作系统线程调用的相关信息。
输入 shift + / 组合键,可以弹出 “使用帮助” 窗口:
例如我们可以按 w 键缩小时间单位粒度,按 s 键放大时间单位粒度。
点击页面的 Goroutine analysis 链接,可以显示每个 goroutine 的编号、调用时长、GMP 调度、网络 IO、阻塞、GC 等方面的数据。
点击 Profiles 区域的链接,可以查看程序的网络 IO、同步、系统调用、GMP 调度等方面的数据。
限于篇幅,页面中其他链接功能这里就不一一展示了,读者可以在 Web UI 页面自行研究。
我们可以在优化代码之前先生成一个 pprof 采样文件,在完成代码优化之后再生成一个 pprof 采样文件,通过比较两者的差异结果来证明我们的优化工作是否有效。 需要注意的是,优化前后必须使用相同的运行环境,保证采样数据的可比性。
# 优化前采样
$ curl -sS "http://127.0.0.1:6060/debug/pprof/profile?seconds=30" -o cpu_before.pprof
# 优化后采样
$ curl -sS "http://127.0.0.1:6060/debug/pprof/profile?seconds=30" -o cpu_after.pprof
# 比较差异 (Web UI)
$ go tool pprof -http=127.0.0.1:6062 --base cpu_before.pprof cpu_after.pprof
上述命令会新启动一个 Web Server (端口为 6062),自动打开浏览器并跳转到 http://127.0.0.1:6062/ui/, 通过 UI 我们可以很直观地看到两个采样数据的差异。
在非 Web 服务中 (如后台服务) 使用 pprof 时,可以使用标准库中的 runtime/pprof 包, 前文示例中的 net/http/pprof 包本质也是对 runtime/pprof 包的一层 Web 封装。
package main
import (
"log"
"math/rand"
"os"
"runtime/pprof"
"sort"
)
func main() {
// 设置采样数据保存文件
file := "/tmp/runtime_cpu.pprof"
f, _ := os.OpenFile(file, os.O_CREATE|os.O_RDWR, 0644)
defer func() {
err := f.Close()
if err != nil {
log.Fatalln(err)
}
}()
// 开始 CPU 采样
err := pprof.StartCPUProfile(f)
if err != nil {
panic(err)
}
defer pprof.StopCPUProfile()
// 每次生成 10000 个随机数字进行排序
for i := 0; i < 1024; i++ {
nums := genRandomNumbers(10000)
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
}
}
// 生成随机数切片
func genRandomNumbers(n int) []int {
nums := make([]int, n)
for i := 1; i < n; i++ {
nums[i] = rand.Int()
}
return nums
}
直接运行:
$ go run main.go
程序执行结束后,会生成一个 CPU 的采样文件: /tmp/runtime_cpu.pprof, 我们继续使用 Web UI 打开:
$ go tool pprof -http=:6062 /tmp/runtime_cpu.pprof
打开后的页面如图所示,功能点在前文 Web UI 小节已经讲过了,这里不再赘述。
除了采样 CPU 的数据,也可以通过指定参数来采样其他数据,因为标准库的采样方法使用起来有些繁琐,我们这里使用开源的 profile[2] 作为演示。
在刚才的例子代码基础上稍作修改,将 CPU 采样改为内存采样,代码如下:
package main
import (
"github.com/pkg/profile"
"math/rand"
"sort"
)
func main() {
// 开始内存采样
defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()
// 每次生成 10000 个随机数字进行排序
for i := 0; i < 1024; i++ {
nums := genRandomNumbers(10000)
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
}
}
// 生成随机数切片
func genRandomNumbers(n int) []int {
nums := make([]int, n)
for i := 1; i < n; i++ {
nums[i] = rand.Int()
}
return nums
}
直接运行:
$ go run main.go
输出结果如下 (具体路径取决于操作系统):
2023/07/29 11:54:55 profile: memory profiling enabled (rate 1), /tmp/profile3144997289/mem.pprof
2023/07/29 11:54:56 profile: memory profiling disabled, /tmp/profile3144997289/mem.pprof
使用 Web UI 打开内存采样文件,数据页面展示如下,读者可以对比一下和 CPU 采样数据页面的差异。
$ go tool pprof -http=:6062 /tmp/profile3144997289/mem.pprof
参考官方的示例代码。
package main
import (
"flag"
"github.com/pkg/profile"
)
func main() {
// use the flags package to selectively enable profiling.
mode := flag.String("profile.mode", "", "enable profiling mode, one of [cpu, mem, mutex, block]")
flag.Parse()
switch *mode {
case "cpu":
defer profile.Start(profile.CPUProfile).Stop()
case "mem":
defer profile.Start(profile.MemProfile).Stop()
case "mutex":
defer profile.Start(profile.MutexProfile).Stop()
case "block":
defer profile.Start(profile.BlockProfile).Stop()
default:
// do nothing
}
}
除了 Web 和后台服务中可以采样,在单元测试中也可以采样:
# CPU 采样
$ go test -v -bench="XXX$" -run='XXX$' -cpuprofile=cpu.pprof .
# 内存 采样
$ go test -v -bench="XXX$" -run='XXX$' -cpuprofile=cpu.pprof .
# trace 采样
$ go test -v -bench="XXX$" -run='XXX$' -trace=trace.out
限于篇幅,数据采样过程和分析过程这里不再演示,感兴趣的读者直接参照前文的示例进行试验。
本文主要讲述了 Go 语言自带的性能分析套件 pprof 和 trace, 再加上以前的这两篇文章 GODEBUG 环境变量详解[6],Go Delve 调试必知必会[7], 可以将这几项工具组合成一把 Go 语言程序调试的 “瑞士军刀”。
官网的安装教程: https://graphviz.org/download/
[2]profile: https://github.com/pkg/profile
[3]这篇文章中提到的: https://dbwu.tech/posts/golang_goroutine_leak/
[4]Gin pprof: https://github.com/gin-contrib/pprof
[5]这个示例程序: https://stackoverflow.com/a/19591389/12203967
[6]GODEBUG 环境变量详解: https://dbwu.tech/posts/golang_godebug/
[7]Go Delve 调试必知必会: https://dbwu.tech/posts/golang_delve/
[8]pprof: https://go.dev/blog/pprof
[9]google/pprof: https://github.com/google/pprof/blob/main/doc/README.md
[10]profile: https://github.com/pkg/profile
[11]Go 语言高性能 Tips: https://dbwu.tech/posts/golang_performance_tips/
[12]fgprof: https://github.com/felixge/fgprof
[13]off cpu analysis: https://www.brendangregg.com/offcpuanalysis.html
[14]pprof++: A Go Profiler with Hardware Performance Monitoring: https://www.uber.com/en-HK/blog/pprof-go-profiler/
[15]GopherCon 2018 - Allocator Wrestling: https://about.sourcegraph.com/blog/go/gophercon-2018-allocator-wrestling
[16]Program Profiling: https://www.practical-go-lessons.com/chap-36-program-profiling
[17]Go 语言-计算密集型服务 性能优化: https://segmentfault.com/a/1190000041602269
[18]Go 语言-IO密集型服务 性能优化: https://segmentfault.com/a/1190000041637173
[19]Go系统性能绕坑工具与方法 - 基于实例的带逛: https://zhuanlan.zhihu.com/p/60042567
[20]go-execution-tracer: https://blog.gopheracademy.com/advent-2017/go-execution-tracer/
[21]go-perfbook: https://github.com/dgryski/go-perfbook
[22]最常用的调试 golang 的 bug 以及性能问题的实践方法: https://www.yuque.com/aceld/golang/ga6pb1#d6b3f4b1
[23]快速定位线上性能问题:Profiling 在微服务应用下的落地实践: https://www.infoq.cn/article/eqazvi015lokje9ejmlk
[24]pprof-web: https://github.com/zjc17/pprof-web/
[25]fgprof: https://github.com/felixge/fgprof
[26]profilinggo: https://github.com/samonzeweb/profilinggo
[27]Cloud Profiler: https://cloud.google.com/profiler
[28]对 Go 程序进行可靠的性能测试: https://changkun.de/research/talks/gobench.pdf
[29]寻找性能瓶颈: https://xargin.com/go-perf-optimization/#%E5%AF%BB%E6%89%BE%E6%80%A7%E8%83%BD%E7%93%B6%E9%A2%88
[30]无人值守的自动 dump(二): https://xargin.com/autodumper-for-go-ii/
[31]self-aware Golang profile dumper: https://github.com/mosn/holmes