在计算机性能调试领域里,profiling是指对应用程序的画像,画像就是应用程序使用CPU和内存的情况。Go语言是一个对性特别看重的语言,因此语言中自带了profiling的库。

Go性能优化

Go语言项目中的性能优化主要有以下几个方面:

  • CPU profile:报告程序的CPU使用情况,按照一定的频率去采集应用程序在CPU和寄存器上面的数据。
  • Memory Profile(Heap Profile):报告程序的内存情况。
  • Block Profile:报告groutine不在运行的状况下,可以用来分析和查找死锁的性能瓶颈。
  • Groutine Profile:报告goroutine的使用情况,有哪些goroutine,他们的调用关系是怎样的。

采集性能数据

Go语言内置了获取程序的运行数据的工具,包括以下两个标准库:

  • runtime/pprof:采集工具型应用运行数据进行分析
  • net/http/pprof:采集服务型应用运行时数据进行分析

pprof开启后,每隔一段时间(10ms)就会收集当前的堆栈信息,获取个个函数占用的CPU以及内存资源,最后通过对这些采样数据进行分析,形成一个性能报告。

工具性应用

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

1
import "runtime/pprof"

CPU性能分析

开启CPU性能分析:

1
pprof.StartCPUProfile(w io.Write)

停止CPU性能分析:

1
pprof.StopCPUProfile()

应用执行结束后,就生成一个文件,保存了我们的CPU profiling数据。得到采样数据后,使用go tool pprof工具进行CPU性能分析。

内存性能优化

记录程序堆栈信息

1
pprof.WriteHeapProfile((w io.Write)

得到采样数据后,使用go tool pprof工具进行性能分析。

go tool pprof默认是使用-inuse_space进行统计,还可以用-inuse-objects查看分配对象的数量。

服务型应用:

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

如果使用了默认的http.DefaultServeMux(通常是代码直接使用http.ListenAndServer("0.0.0.0:8080", ni;)),只需要在你的web server端代码中按如下方式导入net/http/pprof

1
import _ "net/http/pprof"

如果你使用自定义的Mux,则需要手动注册一些路由规则:

1
2
3
4
5
r.HandelFunc("/debug/pprof", pprof.Index)
r.HandelFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandelFunc("/debug/pprof/profile", pprof.Profile)
r.HandelFunc("/debug/pprof/symbol", pprof.Symbol)
r.HandelFunc("/debug/pprof/trace", pprof.Trace)

如果使用GIN框架,推荐使用github.com/DeanThompson/ginpprof

不管什么方式,你的Http服务都会多出/debug/pprofendpoint,访问它会得到以下内容:

prof

这个路径下还有几个子页面:

  • /debug/pprof/profile:访问这个链接会自动进行CPU profiling,持续30s,并生成一个文件提供下载
  • /debug/pprof/heap:Memory Profiling的路径,访问连接会得到一个内存Profiling结果的文件
  • /debug/pprof/block:block Profiling的路径
  • /debug/pprof/goroutine:运行的goroutine列表,以及调用关系。

go tool pprof命令

不管是工具型应用还是服务型应用,我们使用相应的pprof库获取数据之后,下一步都要对这些数据进行分析。

1
go tool pprof [binary] [source]

其中:

  • binary是应用的二进制文件,来解析各种符号。
  • source表示profile数据的来源,可以是本地文件,也可以是http地址。

注意事项:获取的profiling数据是动态的,想要获得有效的数据,请保证应用处于较大的负载(比如正在生成中运行中的服务,或者通过其他工具模拟压力访问)。否则如果应用处于空闲状态,得到的结果可能没有任何意义。

Example:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
"flag"
"fmt"
"os"
"runtime/pprof"
"time"
)

//一段有问题的代码
func logicCode(){
var c chan int //nil
for {
select {
case v := <-c: //阻塞
fmt.Println("recv from chan, value:%v\n", v)
default:
}
}
}

func main() {
var isCpuPprof bool
var isMenPprof bool

flag.BoolVar(&isCpuPprof, "cpu", false, "turn cpu pprof on")
flag.BoolVar(&isMenPprof, "men", false, "turn men pprof on")
flag.Parse()

if isCpuPprof {
f1, err := os.Create("./cpu.pprof")
if err != nil {
fmt.Printf("create cpu pprof failed, err:%v\n ", err)
return
}
pprof.StartCPUProfile(f1)
defer func() {
pprof.StopCPUProfile()
f1.Close()
}()
}
for i := 0; i < 6; i++ {
go logicCode()
}
time.Sleep(20 * time.Second)

if isMenPprof {
f2, err := os.Create("./men.pprof")
if err != nil {
fmt.Printf("create men pprof failed, err:%v\n ", err)
return
}
pprof.WriteHeapProfile(f2)
defer pprof.StopCPUProfile()
}
}

通过flag可以在命令行控制是否开启CPU和Mem的性能分析。

命令行交互界面

使用go工具链中的pprof来分析

1
go tool pprof cpu.pprof

进入交互界面:

1
2
3
4
5
6
03pprof_demo>go tool pprof cpu.pprof
Type: cpu
Time: Aug 20, 2020 at 10:10pm (CST)
Duration: 20.17s, Total samples = 2mins (594.18%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

使用top3来查看程序中占用CPU前3位的函数:

1
2
3
4
5
6
7
(pprof) top 3
Showing nodes accounting for 119.79s, 100% of 119.84s total
Dropped 11 nodes (cum <= 0.60s)
flat flat% sum% cum cum%
59.68s 49.80% 49.80% 98.29s 82.02% runtime.selectnbrecv
38.61s 32.22% 82.02% 38.61s 32.22% runtime.chanrecv
21.50s 17.94% 100% 119.81s 100% main.logicCode

其中:

  • flat:当前函数占用cpu的耗时。
  • flat%:当前函数占用cpu的耗时百分比。
  • sun%:函数占用cpu放入耗时累计百分比。
  • cum:当前函数加上调用当前函数的函数占用cpu的总耗时。
  • cum%:当前函数加上调用当前函数的函数占用cpu的总耗时百分比。
  • 最后一列:函数名称。

可以使用list 函数名命令查看具体的函数分析。例如list logicCode查看函数的具体分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(pprof) list logicCode
Total: 2mins
ROUTINE ======================== main.logicCode in D:\Develop\Sources\GoWorkSpace\src\github.com\DurianLollipop\StudyGo\day09\03pprof_demo\main.go
21.50s 2mins (flat, cum) 100% of Total
. . 11://一段有问题的代码
. . 12:func logicCode(){
. . 13: var c chan int //nil
. . 14: for {
. . 15: select {
21.50s 2mins 16: case v := <-c: //阻塞
. . 17: fmt.Println("recv from chan, value:%v\n", v)
. . 18: default:
. . 19: }
. . 20: }
. . 21:}

图形化

graphviz

go-touch和火焰图

pprof与性能测试结合

go test命令有两个参数和pprof相关,他们分别指定生成cpu和memory profiling

保存的文件:

  • -cpuprofile:cpu profiling数据要保存的文件地址。
  • -memprofile:memory profiling数据要保存的地址。

我们可以将pprof与性能测试相结合,比如:

执行测试的同时,也会执行CPU profiling,并把结果保存在cpu.prof文件中:

1
go test -bench . -cpuprofile=cpu.prof

执行测试的同时,也会执行Mem profiling,并把结果保存在mem.prof文件中:

1
go test -bench . -memprofile=mem.prof

Profiling一般和性能测试一起使用,只有应用在高负载的情况下profiling才有意义。