Golang pprof

Golang pprof教程

Profiling 这个词比较难翻译,一般译成画像。比如在案件侦破的时候会对嫌疑人做画像,从犯罪现场的种种证据,找到嫌疑人的各种特征,方便对嫌疑人进行排查;还有就是互联网公司会对用户信息做画像,通过了解用户各个属性(年龄、性别、消费能力等),方便为用户推荐内容或者广告。

在计算机性能调试领域里,profiling 就是对应用的画像,这里画像就是应用使用 CPU 和内存的情况。也就是说应用使用了多少 CPU 资源?都是哪些部分在使用?每个函数使用的比例是多少?有哪些函数在等待 CPU 资源?知道了这些,我们就能对应用进行规划,也能快速定位性能瓶颈。

Golang pprof详解

类型

Golang 提供了四种 pprof 类型:

类型 描述
CPU profile 报告程序的 CPU 使用情况,按照一定频率去采集应用程序在 CPU 和寄存器上面的数据。
Memory Profile 报告程序的内存使用情况。
Block Profiling 报告 goroutines 不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈。
Goroutine Profiling 报告 goroutines 的使用情况,有哪些 goroutine,它们的调用关系是怎样的。

收集方式

做 Profiling 第一步就是怎么获取应用程序的运行情况数据。go 语言提供了 runtime/pprof 和 net/http/pprof 两个库。同时,按照我们的服务类型,我们可以分为种收集方式:

工具型应用

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

pprof 封装了很好的接口供我们使用,比如要想进行 CPU Profiling,可以调用 pprof.StartCPUProfile() 方法,它会对当前应用程序进行 CPU profiling,并写入到提供的参数中 (w io.Writer),要停止调用StopCPUProfile() 即可。

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

f, err := os.Create(*cpuprofile) ... pprof.StartCPUProfile(f) defer pprof.StopCPUProfile()

应用执行结束后,就会生成一个文件,保存了我们的 CPU profiling 数据。如果,我们想要获得内存的数据,直接使用 WriteHeapProfile 就行,不用 start 和 stop 这两个步骤了:

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

服务型应用

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

如果使用了默认的 http.DefaultServeMux(通常是代码直接使用 http.ListenAndServe(“0.0.0.0:8000”, nil)),只需要添加一行:

import _ "net/http/pprof"

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

r.HandleFunc("/debug/pprof/", pprof.Index) r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) r.HandleFunc("/debug/pprof/profile", pprof.Profile) r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) r.HandleFunc("/debug/pprof/trace", pprof.Trace)

不管哪种方式,你的 HTTP 服务都会多出 /debug/pprof endpoint,访问它会得到类似下面的内容:

/debug/pprof/ profiles: 0 block 62 goroutine 444 heap 30 threadcreate full goroutine stack dump

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

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

go tool pprof命令

go tool pprof 命令用于获取和分析 Profiling 数据。

能通过对应的库获取想要的 Profiling 数据之后(不管是文件还是 http),下一步就是要对这些数据进行保存和分析,我们可以使用 go tool pprof 命令行工具。

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

CPU Profiling

go tool pprof 最简单的使用方式为 go tool pprof [binary] [source],binary 是应用的二进制文件,用来解析各种符号;source 表示 profile 数据的来源,可以是本地的文件,也可以是 http 地址。比如:

go tool pprof -seconds 60 10.20.55.101:9089/debug/pprof/profile Fetching profile over HTTP from http://10.20.55.101:9089/debug/pprof/profile?seconds=60 Please wait... (1m0s) Local symbolization failed for dbmanager: open /home/haicoder/hai/bin/dbmanager: no such file or directory Some binary filenames not available. Symbolization may be incomplete. Try setting PPROF_BINARY_PATH to the search path for local binaries. Saved profile in /root/pprof/pprof.dbmanager.samples.cpu.008.pb.gz File: dbmanager Type: cpu Time: Apr 3, 2019 at 11:20am (CST) Duration: 1mins, Total samples = 12.36s (20.56%) Entering interactive mode (type "help" for commands, "o" for options) (pprof)

这个命令会进行 CPU profiling 分析,等待一段时间(默认是 30s,如果在 url 最后加上 ?seconds=60 参数可以调整采集数据的时间为60s)之后,我们就进入了一个交互式命令行,可以对解析的结果进行查看和导出。可以通过help 查看支持的自命令有哪些。

topN命令

它列出最耗时间的地方:

(pprof) top20 Showing nodes accounting for 6810ms, 55.10% of 12360ms total Dropped 484 nodes (cum <= 61.80ms) Showing top 20 nodes out of 206 flat flat% sum% cum cum% 1160ms 9.39% 9.39% 1350ms 10.92% syscall.Syscall 830ms 6.72% 16.10% 830ms 6.72% runtime.futex 700ms 5.66% 21.76% 1510ms 12.22% runtime.scanobject 480ms 3.88% 25.65% 1660ms 13.43% runtime.mallocgc 430ms 3.48% 29.13% 430ms 3.48% runtime.memclrNoHeapPointers 320ms 2.59% 31.72% 320ms 2.59% runtime.epollwait 300ms 2.43% 34.14% 300ms 2.43% runtime.usleep 280ms 2.27% 36.41% 280ms 2.27% runtime.nextFreeFast 270ms 2.18% 38.59% 270ms 2.18% runtime.heapBitsForObject 260ms 2.10% 40.70% 260ms 2.10% runtime.memmove 260ms 2.10% 42.80% 290ms 2.35% runtime.runqgrab 240ms 1.94% 44.74% 2990ms 24.19% base/redis.(*RedisCon).readReply 200ms 1.62% 46.36% 460ms 3.72% runtime.deferreturn 200ms 1.62% 47.98% 250ms 2.02% runtime.freedefer 180ms 1.46% 49.43% 840ms 6.80% bufio.(*Reader).ReadSlice 160ms 1.29% 50.73% 160ms 1.29% runtime.markBits.isMarked (inline) 140ms 1.13% 51.86% 140ms 1.13% runtime.spanClass.noscan (inline) 140ms 1.13% 52.99% 140ms 1.13% runtime.step 130ms 1.05% 54.05% 160ms 1.29% runtime.heapBitsSetType 130ms 1.05% 55.10% 160ms 1.29% runtime.lock (pprof)

每一行表示一个函数的信息。前两列表示函数在 CPU 上运行的时间以及百分比;第三列是当前所有函数累加使用 CPU 的比例;第四列和第五列代表这个函数以及子函数运行所占用的时间和比例(也被称为累加值 cumulative),应该大于等于前两列的值;最后一列就是函数的名字。

如果应用程序有性能问题,上面这些信息应该能告诉我们时间都花费在哪些函数的执行上了。

pprof 不仅能打印出最耗时的地方(top),还能列出函数代码以及对应的取样数据(list)、汇编代码以及对应的取样数据(disasm),而且能以各种样式进行输出,比如 svg、gv、callgrind、png、gif 等等。

web命令

其中一个非常便利的是 web 命令,在交互模式下输入 web,就能自动生成一个 svg 文件,并跳转到浏览器打开,生成了一个函数调用图。

这个调用图包含了更多的信息,而且可视化的图像能让我们更清楚地理解整个应用程序的全貌。图中每个方框对应一个函数,方框越大代表执行的时间越久(包括它调用的子函数执行时间,但并不是正比的关系);方框之间的箭头代表着调用关系,箭头上的数字代表被调用函数的执行时间。

list命令

要想更细致分析,就要精确到代码级别了,看看每行代码的耗时,直接定位到出现性能问题的那行代码。pprof 也能做到,list 命令后面跟着一个正则表达式,就能查看匹配函数的代码以及每行代码的耗时

(pprof) list main Total: 12.36s ROUTINE ======================== main.(*DbServer).AcInfoFetchByUser in E:/go_server/haicoder-server/src/dbmanager/db.acc.go 0 20ms (flat, cum) 0.16% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.acc.go on path /root/pprof ROUTINE ======================== main.(*DbServer).AppGWLogin in E:/go_server/haicoder-server/src/dbmanager/db.online.go 0 10ms (flat, cum) 0.081% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.online.go on path /root/pprof ROUTINE ======================== main.(*DbServer).ACDot in E:/go_server/haicoder-server/src/dbmanager/db.active.go 0 10ms (flat, cum) 0.081% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.active.go on path /root/pprof ROUTINE ======================== main.(*DbServer).ClearUnReadMsg in E:/go_server/haicoder-server/src/dbmanager/db.stat.go 0 10ms (flat, cum) 0.081% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.stat.go on path /root/pprof ROUTINE ======================== main.(*DbServer).FetchMyAAInfo in E:/go_server/haicoder-server/src/dbmanager/db.answer.go 0 50ms (flat, cum) 0.4% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.answer.go on path /root/pprof ROUTINE ======================== main.(*DbServer).FetchUserAALis in E:/go_server/haicoder-server/src/dbmanager/db.answer.go 0 10ms (flat, cum) 0.081% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.answer.go on path /root/pprof ROUTINE ======================== main.(*DbServer).GetAccount in E:/go_server/haicoder-server/src/dbmanager/dbmanager.login.go 0 20ms (flat, cum) 0.16% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/dbmanager.login.go on path /root/pprof ROUTINE ======================== main.(*DbServer).GetAtList in E:/go_server/haicoder-server/src/dbmanager/db.relation3.go 0 240ms (flat, cum) 1.94% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.relation3.go on path /root/pprof ROUTINE ======================== main.(*DbServer).GetBlackList in E:/go_server/haicoder-server/src/dbmanager/db.relation3.go 0 20ms (flat, cum) 0.16% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.relation3.go on path /root/pprof ROUTINE ======================== main.(*DbServer).GetCommentsList in E:/go_server/haicoder-server/src/dbmanager/db.comments.go 0 20ms (flat, cum) 0.16% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.comments.go on path /root/pprof ROUTINE ======================== main.(*DbServer).GetFansList in E:/go_server/haicoder-server/src/dbmanager/db.relation3.go 0 400ms (flat, cum) 3.24% of Total Error: Could not find file E:/go_server/haicoder-server/src/dbmanager/db.relation3.go on path /root/pprof ROUTINE ======================== main.(*DbServer).GetGoldUser in E:/go_server/haicoder-server/src/dbmanager/db.gold.go 0 10ms (flat, cum) 0.081% of Total

如果想要了解对应的汇编代码,可以使用 disadm <regex> 命令。这两个命令虽然强大,但是在命令行中查看代码并不是很方面,所以你可以使用 weblist 命令,用法和两者一样,但它会在浏览器打开一个页面,能够同时显示源代码和汇编代码。

注意:更详细的 pprof 使用方法可以参考 pprof --help 或者 pprof 文档。

peek命令

peek 是用来查询具体哪个函数的调用关系以及耗时:

(pprof) peek GetUserById Showing nodes accounting for 13.53s, 100% of 13.53s total ----------------------------------------------------------+------------- flat flat% sum% cum cum% calls calls% + context ----------------------------------------------------------+------------- 0.18s 58.06% | main.(*DbServer).GetAtList 0.11s 35.48% | main.(*DbServer).OMGetAAtList 0.01s 3.23% | main.(*DbServer).LikeMe 0.01s 3.23% | main.UserInExitRoom 0 0% 0% 0.31s 2.29% | dbmanager/mongodb.GetUserById 0.31s 100% | dbmanager/mongodb.GetUserInfo ----------------------------------------------------------+------------- 0.50s 100% | main.(*DbServer).GetUserById 0 0% 0% 0.50s 3.70% | dbmanager/mongodb.GetUserByIdAlias 0.50s 100% | dbmanager/mongodb.GetUserInfo ----------------------------------------------------------+------------- 0.50s 100% | protocol/db._DBServerInside_GetUserById_Handler 0 0% 0% 0.50s 3.70% | main.(*DbServer).GetUserById 0.50s 100% | dbmanager/mongodb.GetUserByIdAlias ----------------------------------------------------------+------------- 0.52s 100% | google.golang.org/grpc.(*Server).processUnaryRPC 0 0% 0% 0.52s 3.84% | protocol/db._DBServerInside_GetUserById_Handler 0.50s 96.15% | main.(*DbServer).GetUserById 0.01s 1.92% | google.golang.org/grpc.(*Server).processUnaryRPC.func3 0.01s 1.92% | runtime.assertE2I ----------------------------------------------------------+-------------

Memory Profiling

要想获得内存使用 Profiling 信息,只需要把数据源修改一下就行(对于 http 方式来说就是修改 url 的地址,从 /debug/pprof/profile 改成 /debug/pprof/heap)

go tool pprof -seconds 60 10.20.55.101:9089/debug/pprof/heap Fetching profile over HTTP from http://10.20.55.101:9089/debug/pprof/heap?seconds=60 Please wait... (1m0s) Local symbolization failed for dbmanager: open /home/haicoder/hai/release/bin/dbmanager: no such file or directory Some binary filenames not available. Symbolization may be incomplete. Try setting PPROF_BINARY_PATH to the search path for local binaries. Saved profile in /root/pprof/pprof.dbmanager.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz File: dbmanager Type: inuse_space Time: Apr 3, 2019 at 11:29am (CST) Entering interactive mode (type "help" for commands, "o" for options) (pprof)

和 CPU Profiling 使用一样,使用 top N 可以打印出使用内存最多的函数列表:

(pprof) top20 Showing nodes accounting for 876.41MB, 95.20% of 920.65MB total Dropped 356 nodes (cum <= 4.60MB) Showing top 20 nodes out of 87 flat flat% sum% cum cum% 438.77MB 47.66% 47.66% 438.77MB 47.66% base/redis.NewCmdPool 171.01MB 18.58% 66.23% 267.02MB 29.00% base/redis.(*RedisCon).Do 96MB 10.43% 76.66% 96MB 10.43% base/redis.(*RedisCon).SendToConn 31.50MB 3.42% 80.08% 167.01MB 18.14% common.(*ImplRedisCluster).Exists 29.50MB 3.20% 83.29% 29.50MB 3.20% fmt.Sprintf 24MB 2.61% 85.90% 24MB 2.61% base/redis.(*RedisCon).DoArg1 16MB 1.74% 87.63% 40MB 4.34% base/redis.(*RedisOp).Get 14MB 1.52% 89.15% 84.50MB 9.18% common.(*ImplRedisCluster).GetObject1 10MB 1.09% 90.24% 10MB 1.09% dbmanager/redisdb.checklovekey 9.50MB 1.03% 91.27% 31.50MB 3.42% common.(*ImplRedisCluster).Hget 6.41MB 0.7% 91.97% 6.41MB 0.7% bufio.NewWriterSize (inline) 6MB 0.65% 92.62% 6MB 0.65% dbmanager/redisdb.CRKey 4.61MB 0.5% 93.12% 4.61MB 0.5% bufio.NewReaderSize 4.59MB 0.5% 93.62% 443.36MB 48.16% base/redis.(*RedisCon).init 4.50MB 0.49% 94.11% 10MB 1.09% common.(*ImplRedisCluster).Expire 3.50MB 0.38% 94.49% 69MB 7.50% dbmanager/mongodb.GetUserById 2MB 0.22% 94.71% 5.27MB 0.57% common.(*ImplRedisCluster).Geodist 2MB 0.22% 94.92% 6.50MB 0.71% common.(*ImplRedisCluster).Hincrby 1.50MB 0.16% 95.09% 6.50MB 0.71% common.(*ImplRedisCluster).Hset 1MB 0.11% 95.20% 5.50MB 0.6% base/redis.Args.AddFlat (pprof)

每一列的含义也是类似的,只不过从 CPU 使用时间变成了内存使用大小,就不多解释了。类似的,web 命令也能生成 svg 图片在浏览器中打开,从中可以看到函数调用关系,以及每个函数的内存使用多少。

需要注意的是,默认情况下,统计的是内存使用大小,如果执行命令的时候加上 --inuse_objects 可以查看每个函数分配的对象数;–alloc-space 查看分配的内存空间大小。

这里还要提两个比较有用的方法,如果应用比较复杂,生成的调用图特别大,看起来很乱,有两个办法可以优化:

  • 使用web funcName的方式,只打印和某个函数相关的内容
  • 运行 go tool pprof 命令时加上 --nodefration=0.05 参数,表示如果调用的子函数使用的 CPU、memory 不超过 5%,就忽略它,不要显示在图片中。

和测试工具的集成

在我们使用 go test 进行测试时,我们还有使用相关的参数,来诊断程序的性能:

参数 描述
-cpuprofile cpu profiling 数据要保存的文件地址。
-memprofile memory profiling 数据要报文的文件地址。

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

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

执行结束之后,就会生成 main.test 和 cpu.prof 文件。要想使用 go tool pprof,需要指定的二进制文件就是 main.test。

需要注意的是,Profiling 一般和性能测试一起使用,这个原因在前文也提到过,只有应用在负载高的情况下Profiling 才有意义。

案例

我们输入以下命令,开始收集收集:

go tool pprof -seconds 60 10.20.55.101:9089/debug/pprof/profile Fetching profile over HTTP from http://10.20.55.101:9089/debug/pprof/profile?seconds=60 Please wait... (1m0s) Local symbolization failed for dbmanager: open /home/haicoder/hai/release/bin/dbmanager: no such file or directory Some binary filenames not available. Symbolization may be incomplete. Try setting PPROF_BINARY_PATH to the search path for local binaries. Saved profile in /root/pprof/pprof.dbmanager.samples.cpu.010.pb.gz File: dbmanager Type: cpu Time: Apr 3, 2019 at 4:36pm (CST) Duration: 1mins, Total samples = 16.64s (27.72%) Entering interactive mode (type "help" for commands, "o" for options) (pprof)

使用 web 命令生成 svg 调用图,命令如下:

(pprof) web xdg-open: no method available for opening '/tmp/pprof001.svg' (pprof)

使用 list 命令,查看调用时间,命令如下:

(pprof) list main Total: 16.64s ROUTINE ======================== main.(*DbServer).AddUserSocre in /home/haihai/haicoder-server/src/dbmanager/db.level.go 0 30ms (flat, cum) 0.18% of Total Error: open /home/haihai/haicoder-server/src/dbmanager/db.level.go: no such file or directory ROUTINE ======================== main.(*DbServer).AppGWLogin in /home/haihai/haicoder-server/src/dbmanager/db.online.go 0 10ms (flat, cum) 0.06% of Total Error: open /home/haihai/haicoder-server/src/dbmanager/db.online.go: no such file or directory ROUTINE ======================== main.(*DbServer).AppGWLogout in /home/haihai/haicoder-server/src/dbmanager/db.online.go 0 10ms (flat, cum) 0.06% of Total Error: open /home/haihai/haicoder-server/src/dbmanager/db.online.go: no such file or directory ROUTINE ======================== main.(*DbServer).ACDot in /home/haihai/haicoder-server/src/dbmanager/db.active.go 0 10ms (flat, cum) 0.06% of Total Error: open /home/haihai/haicoder-server/src/dbmanager/db.active.go: no such file or directory ROUTINE ======================== main.(*DbServer).FetchDolist in /home/haihai/haicoder-server/src/dbmanager/db.love.go 0 10ms (flat, cum) 0.06% of Total Error: open /home/haihai/haicoder-server/src/dbmanager/db.love.go: no such file or directory ROUTINE ======================== main.(*DbServer).FetchMyInfo in /home/haihai/haicoder-server/src/dbmanager/db.answer.go 0 60ms (flat, cum) 0.36% of Total Error: open /home/haihai/haicoder-server/src/dbmanager/db.answer.go: no such file or directory ROUTINE ======================== main.(*DbServer).FetchAALis in /home/haihai/haicoder-server/src/dbmanager/db.answer.go 0 10ms (flat, cum) 0.06% of Total Error: open /home/haihai/haicoder-server/src/dbmanager/db.answer.go: no such file or directory ROUTINE ======================== main.(*DbServer).GetAtList in /home/haihai/haicoder-server/src/dbmanager/db.relation3.go 0 260ms (flat, cum) 1.56% of Total

使用 peek 命令,查看函数的具体耗时,命令如下:

(pprof) peek GetUserById Showing nodes accounting for 16.64s, 100% of 16.64s total ----------------------------------------------------------+------------- flat flat% sum% cum cum% calls calls% + context ----------------------------------------------------------+------------- 0.18s 56.25% | main.(*DbServer).GetAtList 0.04s 12.50% | main.(*DbServer).OMGetAAtList 0.03s 9.38% | main.(*DbServer).LikeMe 0.02s 6.25% | main.(*DbServer).GetBlackList 0.02s 6.25% | main.(*DbServer).GetAttEachother 0.01s 3.12% | dbmanager/mongodb.GetAnswerQuestionRankByUID 0.01s 3.12% | main.(*DbServer).GetDoList 0.01s 3.12% | main.(*DbServer).GetUserBill 0 0% 0% 0.32s 1.92% | dbmanager/mongodb.GetUserById 0.32s 100% | dbmanager/mongodb.GetUserInfo ----------------------------------------------------------+------------- 0.61s 89.71% | main.(*DbServer).GetUserById 0.06s 8.82% | main.(*DbServer).RPckShow 0.01s 1.47% | main.PackageList 0 0% 0% 0.68s 4.09% | dbmanager/mongodb.GetUserByIdAlias 0.67s 98.53% | dbmanager/mongodb.GetUserInfo 0.01s 1.47% | runtime.newobject ----------------------------------------------------------+------------- 0.63s 100% | protocol/db._DBServerInside_GetUserById_Handler 0 0% 0% 0.63s 3.79% | main.(*DbServer).GetUserById 0.61s 96.83% | dbmanager/mongodb.GetUserByIdAlias 0.01s 1.59% | protocol.MakeRespBaseInfo 0.01s 1.59% | runtime.newobject ----------------------------------------------------------+------------- 0.66s 100% | google.golang.org/grpc.(*Server).processUnaryRPC 0 0% 0% 0.66s 3.97% | protocol/db._DBServerInside_GetUserById_Handler 0.63s 95.45% | main.(*DbServer).GetUserById 0.03s 4.55% | google.golang.org/grpc.(*Server).processUnaryRPC.func3 ----------------------------------------------------------+-------------