Skip to content

通过 pprof 工具为 Golang 程序进行性能分析

标签
开发/语言/Golang
开发/语言/Golang/性能优化
开发/语言/Golang/Profiling
开发/语言/Golang/工具/pprof
命令行/go
字数
1808 字
阅读时间
8 分钟

准备工作

我们需要从我们的服务程序中提取程序的运行情况。Golang 提供了 runtime/pprofnet/http/pprof 两个库,分别应用在适合的场景中。

一次性应用

如果你的应用是一次性的,运行一段时间就结束。那么最好的办法,就是在应用退出的时候把性能分析的报告保存到文件中,进行分析。对于这种情况,可以使用 runtime/pprof 库。

去除错误处理只需要三行内容,一般把部分内容写在 main.go 文件中,应用程序启动之后就开始执行:

go
    f, err := os.Create(*cpuprofile)

    // 其他代码
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()

应用执行结束后,就会生成一个文件,保存了我们的 CPU profiling 数据。

想要获得内存的数据,直接使用 WriteHeapProfile 就行,不用 start 和 stop 这两个步骤了:

go
    f, err := os.Create(*memprofile)
    pprof.WriteHeapProfile(f)
    f.Close()

服务型应用

如果你的应用是一直运行的,比如 web 应用,那么可以使用 net/http/pprof 库,它能够在提供 HTTP 服务进行分析。

使用 http.DefaultServeMux

在 import 块中导入 net/http/pprof 即可:

go
import _ "net/http/pprof"

使用 Gin 框架或者是其他 HTTP 封装库

go
    r := gin.Default()

    r.Handle(http.MethodGet, "/debug/pprof/", gin.WrapF(pprof.Index))
    r.Handle(http.MethodGet, "/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline))
    r.Handle(http.MethodGet, "/debug/pprof/profile", gin.WrapF(pprof.Profile))
    r.Handle(http.MethodGet, "/debug/pprof/symbol", gin.WrapF(pprof.Symbol))
    r.Handle(http.MethodGet, "/debug/pprof/trace", gin.WrapF(pprof.Trace))

进行分析

直接使用 pprof

有两种方式可以方便我们进行分析,第一种就是直接使用 go 自带的 pprof 命令行工具请求 pprof 的接口:

shell
go tool pprof "localhost:8080/debug/pprof/profile"

来进行 CPU Profiling。

请求之后将会进入一个交互式的命令行,这个时候可以通过在这个交互式命令行窗口中输入 top 来查看排名最前的数据结果。

比如,如果想要分析 heap profile 数据,查看现在 heap 上的内存开销:

shell
 go tool pprof ~/Downloads/heap
File: insights-bot
Type: inuse_space
Time: Dec 11, 2023 at 12:01am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 12127.38kB, 92.21% of 13151.42kB total
Showing top 10 nodes out of 82
      flat  flat%   sum%        cum   cum%
 5768.23kB 43.86% 43.86%  7925.17kB 60.26%  github.com/pkoukk/tiktoken-go.loadTiktokenBpe
 1644.93kB 12.51% 56.37%  1644.93kB 12.51%  strings.genSplit
  809.97kB  6.16% 62.53%   809.97kB  6.16%  bufio.NewWriterSize
  768.26kB  5.84% 68.37%   768.26kB  5.84%  go.uber.org/zap/zapcore.newCounters
  561.50kB  4.27% 72.64%   561.50kB  4.27%  golang.org/x/net/html.map.init.1
  517.33kB  3.93% 76.57%   517.33kB  3.93%  github.com/gookit/color.init256ToHexMap
  517.33kB  3.93% 80.51%   517.33kB  3.93%  regexp/syntax.(*compiler).inst
  515.38kB  3.92% 84.42%   515.38kB  3.92%  ariga.io/atlas/schemahcl.WithTypes
  512.31kB  3.90% 88.32%   512.31kB  3.90%  regexp.onePassCopy
  512.14kB  3.89% 92.21%   512.14kB  3.89%  text/template/parse.New
(pprof)

让我们先回到命令行的使用上,默认情况下,这样的流程会进行 30s,这是因为 pprof 暴露的接口的处理器正在对程序的 CPU 和相关计算资源的用量进行快照采集,静静等待结束之后 pprof 就会进入交互式界面让我们交互式浏览 pprof 的结果。 当然你完全可以修改这样的默认行为,你可以要求 pprof 的快照时长延长为 60s。

shell
go tool pprof "localhost:8080/debug/pprof/profile?seconds=60"

快照?pprof 的底层原理

简而言之,快照在这里的含义其实并不是简单地将 Profiling 的过程中这段时间的函数调用和调用耗时快照下来,更复杂的是,pprof 其实是在默认配置情况下的 30s 的时间窗口内,将这段时间范围内的高耗时函数调用,以及计算流程像是「拍个照片」一样记录下来。

复杂一点说,pprof 命令行将会在被执行的时候自动找参数提供的 Profiling 接口(在上面的例子中则为 localhost:8080/debug/pprof/profile)发起 GET 请求,在 debug/pprof/profile 接收到请求之后,将会高耗时函数调用,内存分配,符号申请,栈内栈外的信息做提取和快照操作,然后打包返回一个 pprof 可以读取和分析的二进制数据包,最终到交互式的 pprof 操作和方便用户和开发者观测这些关键的、足够显眼的调用栈和消耗。

因此,如果你想要通过 pprof 去细致入微地测量每个函数的调用的话,是行不通的,因为他们对于 pprof 而言不够显眼,换句话说,是占用的计算资源不如别的函数,这个时候 pprof 接口所返回的 Profiling 数据中是不会包含这些信息的。

对于精确测量函数调用的需求,请使用 Jeager 或者我撰写的 nekomeowww/elapsing 库来对逐个函数和调用栈进行可观测分析。

如果有二进制文件,或者想看 pprof 的源代码映射的话,也可以在第二个参数中指定正在 pprof 的二进制文件:

shell
go tool pprof ./main "localhost:8080/debug/pprof/profile?seconds=60"

先缓存下来再 pprof

在上面的例子中介绍的 pprof 的使用方法都是非常快捷临时的用法,很多时候我们不能每次都去环境里面安装 go 或者是在不保存结果的情况下对 pprof 接口返回的结果进行分析,这个时候需要我们通过别的方式实现这样的功能。

正如在 pprof 的底层原理中讲述的

pprof 命令行将会在被执行的时候自动找参数提供的 Profiling 接口(在上面的例子中则为 localhost:8080/debug/pprof/profile)发起 GET 请求,在 debug/pprof/profile 接收到请求之后,将会高耗时函数调用,内存分配,符号申请,栈内栈外的信息做提取和快照操作,然后打包返回一个 pprof 可以读取和分析的二进制数据包

所以你完全可以通过诸如 wgetcurl 这样别的 HTTP 客户端对接口进行 GET 请求并将 pprof 可读取和分析的二进制存储到本地,然后在有 pprof 的环境中分析和执行进一步的 debug 和挖掘操作,或者是方便之后的二次复盘和数据对比:

shell
wget localhost:8080/debug/pprof/profile
shell
curl localhost:8080/debug/pprof/profile -O

如果是从 Docker 容器中获取的 pprof profile 资料文件,可以通过下面的命令复制到宿主机:

shell
sudo docker cp <container hash>:<path to pprof profile file> ~/

然后再执行

shell
go tool pprof <path to pprof profile file>

可视化分析

使用 pprof 工具可以直接输出一个可运行的网页,在网页中可以选择查看调用统计、调用次序、火焰图等。

安装额外的依赖

要渲染这些图片,我们还需要在 macOS 上安装 graphviz:

bash
brew install graphviz

输入 profile 文件并打开一个网页服务器

bash
go tool pprof -http=":8080" ./main ~/pprof/pprof.main.samples.cpu.001.pb.gz

这个时候在浏览器中打开 http://localhost:8080,就可以看到性能分析的数据了。

延伸阅读

使用 pprof 和火焰图调试 golang 应用 | Cizixs Write Here

Golang 大杀器之性能剖析 PProf - SegmentFault 思否

Golang remote profiling and flamegraphs · matoski.com

相关资源

Download | Graphviz

贡献者

页面历史

撰写

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。