pprof:go 的性能调试工具


查一个内存泄漏问题时,使用到了 pprof 工具,在此介绍一下。


简要介绍

pprof 是用于分析 go 程序的工具(分析内存、CPU、mutex、goroutine 等),并提供可视化能力。

使用 pprof 前,需要在代码中引用 "net/http/pprof",并暴露一个端口,下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 死循环往一个 map 中加入元素

package main

import (
"math/rand"
"net/http"
_ "net/http/pprof" // 这里要引用
)

func main() {
go http.ListenAndServe(":8080", nil) // 暴露一个端口出来,供 pprof 调用
func1()
}

func func1() {
func2()
}

func func2() {
func3()
}

func func3() {
m := make(map[int]bool)
for {
m[rand.Intn(100000000)] = true
}
}

执行程序后,可以通过 go tool pprof 进行分析,查看哪部分代码 CPU 占用高、内存占用高等等。例如下图就是通过 pprof 得到的内存占用情况,可以看出所有内存占用都出自 func3 方法:

pprof 查看内存


上手使用

程序启动后,有两种常用的 pprof 使用方式:

  • 网页操作(需要提前安装好 graphviz)

    1
    go tool pprof -http=:8000 http://localhost:8080/debug/pprof/heap

    执行后会自动跳出浏览器并打开 http://localhost:8000/ui/
    pprof 网页操作

  • 交互式命令行操作

    1
    go tool pprof http://localhost:8080/debug/pprof/heap

    执行后会进入到 pprof 的交互模式,之后使用命令行交互

    pprof 命令行操作

网页操作比较简单,四处点点就明白了,下面主要介绍命令行交互操作。

基本原理

pprof 会把一系列的程序分析样本(profiling samples)打包保存成 proto 文件,并基于这些数据生成文本和图片。

pprof 有自己的命令(pprof [options] source),但是使用起来有点麻烦,我们借助 go tool 来操作 pprof。当我们调用命令 go tool pprof http://localhost:8080/debug/pprof/heap 时,pprof 会首先生成 proto 文件,并让我们进入到命令行交互模式,查看这些数据。

1
2
3
4
5
6
7
8
9
10
11
12
# 输入命令
go tool pprof http://localhost:8080/debug/pprof/heap

# 一些提示信息,可以看到生成了 pb.gz 文件,即 proto 格式的采样数据
Fetching profile over HTTP from http://localhost:8080/debug/pprof/heap
Saved profile in /Users/pz/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
Type: inuse_space
Time: Oct 8, 2022 at 5:00pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)

# 命令行交互
(pprof)

我们在执行 go tool pprof 命令时,使用的 url 地址结尾是 heap,这代表对内存进行采样。除了 heap 之外还有其他一些可选值,例如 allocs(所有曾经分配过的内存)、goroutine(协程数量)、profile(CPU 占用)等等,可自行参考 http://localhost:8080/debug/pprof/

可视化

pprof 通过 graphviz 画图,因此你需要事先安装好 graphviz。安装 graphviz 很简单,基本都是一行命令完事,可以参考官方文档:《graphviz - Download》。

顺便一提,通过交互式命令行也可以生成可视化图片,进入交互模式输入 web 即可(会生成 svg 文件并打开):

1
(pprof) web

常用命令

pprof 有非常多命令,你可以在交互模式下输入 help 查看,这里只介绍个人认为常用的命令:

  • top(查看消耗排前 10 的项)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    (pprof) top
    Showing nodes accounting for 1.16GB, 99.79% of 1.17GB total
    Dropped 15 nodes (cum <= 0.01GB)
    flat flat% sum% cum cum%
    1.16GB 99.79% 99.79% 1.16GB 99.79% main.func3 (inline)
    0 0% 99.79% 1.16GB 99.79% main.func1 (inline)
    0 0% 99.79% 1.16GB 99.79% main.func2 (inline)
    0 0% 99.79% 1.16GB 99.79% main.main
    0 0% 99.79% 1.17GB 100% runtime.main
  • top3(查看消耗排前 N 的项)

    1
    2
    3
    4
    5
    6
    7
    8
    (pprof) top3
    Showing nodes accounting for 1.16GB, 99.79% of 1.17GB total
    Dropped 15 nodes (cum <= 0.01GB)
    Showing top 3 nodes out of 5
    flat flat% sum% cum cum%
    1.16GB 99.79% 99.79% 1.16GB 99.79% main.func3 (inline)
    0 0% 99.79% 1.16GB 99.79% main.func1
    0 0% 99.79% 1.16GB 99.79% main.func2 (inline)
  • top -cum(累加模式下的 top)

    1
    2
    3
    4
    5
    6
    7
    8
    Showing nodes accounting for 1.16GB, 99.79% of 1.17GB total
    Dropped 15 nodes (cum <= 0.01GB)
    flat flat% sum% cum cum%
    0 0% 0% 1.17GB 100% runtime.main
    0 0% 0% 1.16GB 99.79% main.func1 (inline)
    0 0% 0% 1.16GB 99.79% main.func2 (inline)
    1.16GB 99.79% 99.79% 1.16GB 99.79% main.func3 (inline)
    0 0% 99.79% 1.16GB 99.79% main.main
  • list xxx(显示源代码)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    (pprof) list func3
    Total: 1.17GB
    ROUTINE ======================== main.func3 in /Users/pz/Desktop/pz_learn/go_learn/main.go
    1.16GB 1.16GB (flat, cum) 99.79% of Total
    . . 31:}
    . . 32:
    . . 33:func func3() {
    . . 34: m := make(map[int]bool)
    . . 35: for {
    1.16GB 1.16GB 36: m[rand.Intn(100000000)] = true
    . . 37: }
    . . 38:}
  • web(生成 svg 图片并打开)

    1
    (pprof) web

inline

有时你能在 pprof 的分析中看到 inline,这代表内联,意味着 go 在编译期把这个函数干掉了,插入到调用该函数的地方,从而节省调用函数的开销。

如果你不希望函数内联,可以在函数前面插入 //go:noinline 注释。

以之前的代码为例,我们禁止 func3 函数内联,应该这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"math/rand"
"net/http"
_ "net/http/pprof"
)

func main() {
go http.ListenAndServe(":8080", nil)
func1()
}

func func1() {
func2()
}

func func2() {
func3()
}

//go:noinline
func func3() {
m := make(map[int]bool)
for {
m[rand.Intn(100000000)] = true
}
}

看一下 pprof 的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 禁用内联前
(pprof) top -cum
Showing nodes accounting for 595.55MB, 99.73% of 597.14MB total
Dropped 12 nodes (cum <= 2.99MB)
flat flat% sum% cum cum%
0 0% 0% 597.14MB 100% runtime.main
0 0% 0% 595.55MB 99.73% main.func1 (inline)
0 0% 0% 595.55MB 99.73% main.func2 (inline)
595.55MB 99.73% 99.73% 595.55MB 99.73% main.func3 (inline)
0 0% 99.73% 595.55MB 99.73% main.main

# 禁用内联后
(pprof) top -cum
Showing nodes accounting for 1.16GB, 100% of 1.16GB total
flat flat% sum% cum cum%
0 0% 0% 1.16GB 100% main.func1 (inline)
0 0% 0% 1.16GB 100% main.func2 (inline)
1.16GB 100% 100% 1.16GB 100% main.func3
0 0% 100% 1.16GB 100% main.main
0 0% 100% 1.16GB 100% runtime.main

可以看到 main.func3 后面少了 (inline)


tips

最后加个小 tips:如果程序通过 docker 启动,可以先进到容器里,再执行 go tool pprof ...

1
2
docker exec -it {ContainerID} bash
go tool pprof ...

pprof docker操作

不过因为没有 graphviz,也就这样没办法看图了。


参考文档