在 Go 中内置的几种分析器都是为使用 pprof(https://github.com/google/pprof)的可视化工具而设计的。pprof 本身不是谷歌的官方项目,pprof 一开始设计的目的是分析来自 c++、Java 和 Go 程序的数据。该项目使用 protocol buffer 协议定义了所有 Go 分析器的格式,本文都会对他们进行描述。

go tool pprofgo tool pprof

特点

pprof 工具提供一个交互式命令行界面,一个 Web UI,还包括各种其他输出格式选项。

文件格式

描述

pprof 相关的格式定义在 profile.proto (https://github.com/google/pprof/blob/master/proto/profile.proto)文件中, 这是一个使用 protocol buffer 协议定义的文件,并且这个文件带有比较全面的注释。另外,还有一个官方的说明(https://github.com/google/pprof/blob/master/proto/README.md) 。pprof 保存在磁盘的文件使用的是 gzip 格式压缩的。

俗话说,一张图胜过千言万语,下面是 pprof 工具的可视化格式,这张图片是使用protodot(https://github.com/seamia/protodot) 工具自动生成的。

pprof 的数据格式似乎是为效率、多语言 (编程语言) 和不同的性能分析类型 (CPU、堆等) 而设计的,但正因为如此,它非常抽象,显得不直观。如果你想了解所有细节,请点击这里(https://github.com/google/pprof/blob/master/proto/profile.proto) 。继续向下阅读也能知道这么设计的原因。pprof 文件包含采集的堆栈列表,这些堆栈信息具有一个或多个与之关联的数值。比如对于 CPU 性能分析,该值可能是在性能分析期间观察到堆栈的 CPU 持续时间(以纳秒为单位)。对于堆性能分析,它可能是分配的字节数。值类型本身在文件的开头进行了描述,并用于填充 pprof UI 中的 “SAMPLE” 下拉列表。除了值之外,每个堆栈跟踪还可以包括一组标签。标签是键值对,甚至可以包含一个单元。在 Go 中,这些标签应用在分析器标签(https://rakyll.org/profiler-labels/) 上 。

性能分析还包括记录性能分析的时间(UTC 格式)以及记录的持续时间。

另外,这个格式允许删除/保留正则表达式以排除/包括某些堆栈跟踪信息,但 Go 不使用(https://github.com/golang/go/blob/go1.15.6/src/runtime/pprof/proto.go#L375-L376) 它们。还有一个注释列表(也没有使用(https://github.com/golang/go/search?q=tagProfile_Comment)),以及描述采样的周期间隔时间。

在 Go 中生成 pprof 输出的代码可以在:runtime/pprof/proto.go(https://github.com/brendangregg/FlameGraph#2-fold-stacks) 中找到。

解码

下面是一些用于将 pprof 文件解码为人类可读文本输出的工具。它们按输出格式的复杂程度排序,显示简化输出的工具列在最前面:

使用 pprofutils 来进行辅助分析

pprofutils(https://github.com/felixge/pprofutils) 是一个用于在 pprof 文件和 Brendan Gregg 的折叠文本(https://github.com/felixge/pprofutils) 格式之间转换的小工具。你可以这样使用它

$ pprof2text < examples/cpu/pprof.samples.cpu.001.pb.gz

golang.org/x/sync/errgroup.(*Group).Go.func1;main.run.func2;main.computeSum 19
golang.org/x/sync/errgroup.(*Group).Go.func1;main.run.func2;main.computeSum;runtime.asyncPreempt 5
runtime.mcall;runtime.gopreempt_m;runtime.goschedImpl;runtime.schedule;runtime.findrunnable;runtime.stopm;runtime.notesleep;runtime.semasleep;runtime.pthread_cond_wait 1
runtime.mcall;runtime.park_m;runtime.schedule;runtime.findrunnable;runtime.checkTimers;runtime.nanotime;runtime.nanotime1 1
runtime.mcall;runtime.park_m;runtime.schedule;runtime.findrunnable;runtime.stopm;runtime.notesleep;runtime.semasleep;runtime.pthread_cond_wait 2
runtime.mcall;runtime.park_m;runtime.resetForSleep;runtime.resettimer;runtime.modtimer;runtime.wakeNetPoller;runtime.netpollBreak;runtime.write;runtime.write1 7
runtime.mstart;runtime.mstart1;runtime.sysmon;runtime.usleep 3

使用 go tool pprof 命令

pprof-raw
$ go tool pprof -raw examples/cpu/pprof.samples.cpu.001.pb.gz

PeriodType: cpu nanoseconds
Period: 10000000
Time: 2021-01-08 17:10:32.116825 +0100 CET
Duration: 3.13
Samples:
samples/count cpu/nanoseconds
         19  190000000: 1 2 3
          5   50000000: 4 5 2 3
          1   10000000: 6 7 8 9 10 11 12 13 14
          1   10000000: 15 16 17 11 18 14
          2   20000000: 6 7 8 9 10 11 18 14
          7   70000000: 19 20 21 22 23 24 14
          3   30000000: 25 26 27 28
Locations
     1: 0x1372f7f M=1 main.computeSum /Users/felix.geisendoerfer/go/src/github.com/felixge/go-profiler-notes/examples/cpu/main.go:39 s=0
     2: 0x13730f2 M=1 main.run.func2 /Users/felix.geisendoerfer/go/src/github.com/felixge/go-profiler-notes/examples/cpu/main.go:31 s=0
     3: 0x1372cf8 M=1 golang.org/x/sync/errgroup.(*Group).Go.func1 /Users/felix.geisendoerfer/go/pkg/mod/golang.org/x/sync@v0.0.0-20201207232520-09787c993a3a/errgroup/errgroup.go:57 s=0
     ...
Mappings
1: 0x0/0x0/0x0   [FN]

上面的样例没有给出全部的内容,点击这里查看所有的内容(https://github.com/gperftools/gperftools/blob/master/src/pprof)。

Using protoc

使用 protoc

brew install protobuf

现在让我们来分析上面的 CPU 的性能分析数据:

$ gzcat examples/cpu/pprof.samples.cpu.001.pb.gz | protoc --decode perftools.profiles.Profile ./profile.proto

sample_type {
  type: 1
  unit: 2
}
sample_type {
  type: 3
  unit: 4
}
sample {
  location_id: 1
  location_id: 2
  location_id: 3
  value: 19
  value: 190000000
}
sample {
  location_id: 4
  location_id: 5
  location_id: 2
  location_id: 3
  value: 5
  value: 50000000
}
...
mapping {
  id: 1
  has_functions: true
}
location {
  id: 1
  mapping_id: 1
  address: 20393855
  line {
    function_id: 1
    line: 39
  }
}
location {
  id: 2
  mapping_id: 1
  address: 20394226
  line {
    function_id: 2
    line: 31
  }
}
...
function {
  id: 1
  name: 5
  system_name: 5
  filename: 6
}
function {
  id: 2
  name: 7
  system_name: 7
  filename: 6
}
...
string_table: ""
string_table: "samples"
string_table: "count"
string_table: "cpu"
string_table: "nanoseconds"
string_table: "main.computeSum"
string_table: "/Users/felix.geisendoerfer/go/src/github.com/felixge/go-profiler-notes/examples/cpu/main.go"
...
time_nanos: 1610122232116825000
duration_nanos: 3135113726
period_type {
  type: 3
  unit: 4
}
period: 10000000

这份输出样例也是没有给出全部的内容,点击这里查看所有的内容。

历史

最初的 pprof 工具(https://github.com/gperftools/gperftools/blob/master/src/pprof) 是谷歌内部使用 perl 开发的脚本。根据版权标题,开发可能要追溯到 1998 年。它于 2005 年作为 gperftools (https://github.com/google/tcmalloc/blob/master/docs/gperftools.md)的一部分首次发布,并于 2010 年 添加 (https://github.com/golang/go/commit/8b5221a57b41a19abcb4e3dde20014af494048c2)到 Go 项目中。

2014 年,Go 项目用 Raul Silvera 的 Go 实现取代了(https://github.com/golang/go/commit/8b5221a57b41a19abcb4e3dde20014af494048c2)基于 perl 的 pprof 工具,目前谷歌已经使用了这个实现的工具。这个实现在 2016 年作为一个独立的项目(https://github.com/google/pprof) 进行重新发布。从那时起,Go 项目一直在提供上游项目的 pprof,并定期对其进行 更新(https://github.com/golang/go/commits/master/src/cmd/vendor/github.com/google/pprof) 。

Go 1.9(2017-08-24) 增加了对 pprof 标签的支持。它还开始在默认情况下将符号信息包含到 pprof 文件中,这允许在不访问二进制文件的情况下查看程序性能。

Todo

go tool pprof

免责声明

我是 felixge ,在Datadog 从事关于 go 代码性能分析 的工作。你应该去看看。我们也在 招聘 :)。

本页上的信息不保证是正确的。我们欢迎你的反馈。

原文信息

本文永久链接:https://github.com/gocn/translator/blob/master/2021/w39_go_profiler_notes_pprof_tool_format.md

译者:小超人

校对:fivezh、lsj1342

想要了解关于 Go 的更多资讯,还可以通过扫描的方式,进群一起探讨哦~