Golang pprof 性能分析指南

pprof 是一个用于可视化和分析分析数据的工具。

采样方式

方式名称如何使用优点缺点使用场景
runtime/pprof手动调用【runtime.StartCPUProfile、runtime.SweightCPUProfile】等API来进行数据的采集。采集程序(非 Server)的指定区块的运行数据进行分析。灵活性高、按需采集。工具型应用(比如说定制化的分析小工具、集成到公司监控系统)。这种应用运行一段时间就结束。
net/http/pprof通过http服务来获取Profile采样文件。 import _ "net/http/pprof"。基于 HTTP Server 运行,并且可以采集运行时数据进行分析。net/http/pprof中只是使用runtime/pprof包来进行封装了一下,并在http端口上暴露出来简单易用在线服务(一直运行着的程序)
go test通过命令go test -bench . -cpuprofile cpu.prof来进行采集数据。针对性强、细化到函数进行某函数的性能测试

指标解释

350959005-c2182bee-333f-4ec3-94cb-7a36440bb105

常用指标如下:

  • allocs:所有时刻的内存使用情况,包括正在使用的及已经回收的

  • block:导致在同步原语上发生阻塞的堆栈跟踪

  • cmdline: 当前程序的命令行的完整调用路径。

  • goroutine:目前的 goroutine 数量及运行情况

  • heap:当前时刻的内存使用情况

  • mutex:查看导致互斥锁的竞争持有者的堆栈跟踪

  • profile:默认进行 30s 的 CPU Profiling,得到一个分析用的 profile 文件

  • threadcreate:查看创建新 OS 线程的堆栈跟踪。

  • trance:当前程序执行的追踪,可以在秒数的 GET 参数中指定持续时间。在获取追踪文件后,请使用 go 工具的 trace 命令来调查追踪。(深入浅出 Go trace (qq.com)

注意,默认情况下是不追踪block和mutex的信息的,如果想要看这两个信息,需要在代码中加上两行:

1
2
runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪,block  
runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪,mutex
注意,上文的所有信息都是实时的,如果你刷新一下,是可以看到数字在变化的。此时如果点击蓝色的连接,可以看到一些协程的栈信息,这些信息并不容易阅读。如果想要更加清晰的数据,需要将信息保存下来,在本地进行分析。

理解指标

  • flat:函数自身的运行耗时。
  • flat%:函数自身在 CPU 运行耗时总比例。
  • sum%:函数自身累积使用 CPU 总比例。
  • cum:函数自身及其调用函数的运行总耗时。
  • cum%:函数自身及其调用函数的运行耗时总比例。

flat flat%

一个函数内的 directly 操作的物理耗时。例如

flat只会记录 step2 和 step3 的时间

1
2
3
4
5
6
7
func foo(){
     a()                                        // step1
     largeArray := [math.MaxInt64]int64{}       // step2
     for i := 0; i < math.MaxInt64; i++ {       // step3
             c()                                // step4
     }
 }
flat%即是 `flat/总运行时间`。内存等参数同理。**所有的 flat 相加即是总采样时间**,所有的flat%相加应该等于100%。

flat一般是我们最关注的。其代表一个函数可能非常耗时,或者调用了非常多次,或者两者兼而有之,从而导致这个函数消耗了最多的时间。

如果是我们自己编写的代码,则很可能有一些无脑 for 循环、复杂的计算、字符串操作、频繁申请内存等等。

如果是第三方库的代码,则很可能我们过于频繁地调用了这些第三方库,或者以不正确的方式使用了这些第三方库。

cum cum%

相比 flat,cum则是这个函数内所有操作的物理耗时,比如包括了上述的 step1、2、3、4。

cum%即是`cum的时间/总运行时间`。内存等参数同理。

一般cum是我们次关注的,且需要结合flat来看。

flat 可以让我们知道哪个函数耗时多,而 cum 可以帮助我们找到是哪些函数调用了这些耗时的(flat 值大的)函数。

sum%

其上所有行的flat%的累加。可以视为,这一行及其以上行,其所有的 directly 操作一共占了多少物理时间。

案例分析

go test

  • cpu 使用分析:-cpuprofile=cpu.pprof
  • 内存使用分析:-benchmem -memprofile=mem.pprof
  • block分析:-blockprofile=block.pprof
1
2
3
4
go test -bench=. -run=none -benchmem -memprofile=mem.pprof 
go test -bench=. -run=none -blockprofile=block.pprof 
go test -bench=. -run=none -benchmem -memprofile=mem.pprof -cpuprofile=cpu.pprof s
go test -bench=. -run=none -benchmem -memprofile=mem.pprof -cpuprofsile=cpu.pprof -blockprofile=block.pprof

runtime/pprof

除了注入 http handler 和 go test 以外,我们还可以在程序中通过 pprof 所提供的 Lookup 方法来进行相关内容的采集和调用,其一共支持六种类型,分别是:goroutine、threadcreate、heap、block、mutex,代码如下:

 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
type LookupType int8 

const ( 
    LookupGoroutine LookupType = iota 
    LookupThreadcreate LookupHeap 
    LookupAllocs LookupBlock LookupMutex ) 

func pprofLookup(lookupType LookupType, w io.Writer) error { 
    var err error 
    switch lookupType { 
        case LookupGoroutine: 
        	p := pprof.Lookup("goroutine") 
        err = p.WriteTo(w, 2) 
    	case LookupThreadcreate: 
        	p := pprof.Lookup("threadcreate") 
        err = p.WriteTo(w, 2) 
        case LookupHeap: 
        	p := pprof.Lookup("heap") 
        err = p.WriteTo(w, 2) 
        case LookupAllocs: 
        	p := pprof.Lookup("allocs") 
        	err = p.WriteTo(w, 2) 
        case LookupBlock: 
        	p := pprof.Lookup("block") 
        	err = p.WriteTo(w, 2) 
        case LookupMutex: 
        	p := pprof.Lookup("mutex") 
        	err = p.WriteTo(w, 2) 
    	} 
    return err 
}

接下来我们只需要对该方法进行调用就好了,其提供了 io.Writer 接口,也就是只要实现了对应的 Write 方法,我们可以将其写到任何支持地方去,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func init() { 
    runtime.SetMutexProfileFraction(1) 
    runtime.SetBlockProfileRate(1) 
} 

func main() { 
    http.HandleFunc("/lookup/heap", func(w http.ResponseWriter, r *http.Request) {
        _ = pprofLookup(LookupHeap, os.Stdout) }) 
    http.HandleFunc("/lookup/threadcreate", func(w http.ResponseWriter, r *http.Request) { 
        _ = pprofLookup(LookupThreadcreate, os.Stdout) }) 
    http.HandleFunc("/lookup/block", func(w http.ResponseWriter, r *http.Request) { 
        _ = pprofLookup(LookupBlock, os.Stdout) }) 
    http.HandleFunc("/lookup/goroutine", func(w http.ResponseWriter, r *http.Request) { 
        _ = pprofLookup(LookupGoroutine, os.Stdout) }) 
   
    _ = http.ListenAndServe("0.0.0.0:6060", nil) 
}

采集CPU(CPU 占用过高)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func CollectCpu() { 
	// 创建分析文件,在当前目录下 
	file, err := os.Create("./cpu.prof") 
	if err != nil { 
		fmt.Printf("创建采集文件失败, err:%v\n", err) 
		return 
	} 
	// 进行 cpu 数据的获取 
	pprof.StartCPUProfile(file) 
	defer pprof.SweightCPUProfile() 
	// 执行一段有问题的代码 
	for i := 0; i < 4; i++ { s
		go do1() 
	} 
	time.Sleep(10 * time.Second) 
}

net/http/pprof

1
2
 # 网页,运行该命令让程序开始半分钟(默认值)的CPU采样 服务开启了 pprof
 $ go tool pprof http://127.0.0.1:8080/debug/pprof/profile

350959401-d56c6bdc-d272-4960-8ac0-943f42b46e54

扩展参数选项

选项名作用
alloc_objects分析应用程序的内存临时分配情况
alloc_space查看每个函数分配的内存空间大小
inuse_space分析应用程序的常驻内存占用情况
inuse_objects查看每个函数所分配的对象数量

常用命令

web (连线图)

通过Web浏览器可视化图

web 将会生成一张svg格式的图片,并用默认打开程序打开(一般是浏览器)。渲染图片需要下载 Download | Graphviz

350959152-f919098b-a923-4cbd-8349-89f0e3a0d52b

节点的颜色越红,其cum和cum%越大。其颜色越灰白,则cum和cum%越小。

节点越大,其flat和flat%越大;其越小,则flat和flat%越小

线条代表了函数的调用链,线条越粗,代表指向的函数消耗了越多的资源。反之亦然。

线条的样式代表了调用关系。实线代表直接调用;虚线代表中间少了几个节点;带有inline字段表示该函数被内联进了调用方(不用在意,可以理解成实线)。

对于一些代码行比较少的函数,编译器倾向于将它们在编译期展开从而消除函数调用,这种行为就是内联。

weight

weight默认按flat排序,打印出消耗前10的函数。也可以选择消耗前N的函数,比如weight5weight20

350959570-a1c83317-3d82-4bfe-aaf7-4abfebd1884f

list

当发现某个函数资源占用情况可疑时,可以通过 list 函数名 定位到具体的代码位置。比如: list Fire

350959588-87007000-e59a-43d1-bcce-2c8e5fe79a9c

火焰图

调用顺序**由上到下**,每一块代表一个函数,越大代表占用 CPU 的时间更长。同时它也可以支持点击块深入进行分析。

350959621-4992c35f-ac6f-4e44-8246-a99ce6302bfb

代码优化建议

以下是一些从其它项目借鉴或者自己总结的实践经验,它们只是建议,而不是准则,实际项目中应该以性能分析数据来作为优化的参考,避免过早优化

  1. 对频繁分配的小对象,使用 sync.Pool 对象池避免分配
  2. 自动化的 DeepCopy 是非常耗时的,其中涉及到反射,内存分配,容器(如 map)扩展等,大概比手动拷贝慢一个数量级
  3. 用 atomic.Load/StoreXXX,atomic.Value, sync.Map 等代替 Mutex。(优先级递减)
  4. 使用高效的第三方库,如用fasthttp替代 net/http
  5. 在开发环境加上-race编译选项进行竞态检查
  6. 在开发环境开启 net/http/pprof,方便实时 pprof
  7. 将所有外部IO(网络IO,磁盘IO)做成异步ss

参考文章