在计算机性能调试领域里,profile 就是对应用的画像,这里画像就是应用使用 CPU 和内存等情况,也就是说应用使用了多少 CPU 资源、都是哪些部分在使用、每个函数使用的比例是多少、有哪些函数在等待 CPU 资源等等。知道了这些,我们就能对应用进行规划,也能快速定位性能瓶颈。
Golang 是一个对性能特别看重的语言,因此语言中自带了 profile 的库,这篇文章就要讲解怎么在 golang 中做 profile。
在 Golang 中,主要关注的应用运行情况主要包括以下几种:
分析 profile 第一步就是怎么获取应用程序的运行情况数据。Golang 提供了 runtime/pprof
和 net/http/pprof
两个库,分别应用于两种不同的应用。
如果你的应用是一次性的,运行一段时间就结束,那么最好的办法就是在应用退出时把 profile 的报告保存到文件中,进行分析。对于这种情况,可以使用 runtime/pprof
库。
pprof
封装了很好的接口供我们使用,比如要想进行 CPU profile,可以调用 pprof.StartCPUProfile()
方法,它会对当前应用程序进行 CPU profile,并写入到提供的参数中(w io.Writer
)。要停止写入,调用 StopCPUProfile()
即可。
去除错误处理只需要三行内容,一般把它们写在 main.go
文件中,应用程序启动之后就开始执行:
f, err := os.Create(*cpuprofile)
...
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
应用执行结束后,就会生成一个文件,保存了我们的 CPU profile 数据。
要获得内存的数据,直接使用 WriteHeapProfile
即可,不用 start
和 stop
这两个步骤:
f, err := os.Create(*memprofile)
pprof.WriteHeapProfile(f)
f.Close()
如果你的应用是一直运行的,比如 web 应用,那么可以使用 net/http/pprof
库,它能够对 Http 服务进行分析。
在 import 里添加一行:
import _ "net/http/pprof"
在主函数中启动服务监听端口:
go func() {
http.ListenAndServe(":6060", nil)
}()
访问 /debug/pprof
即可得到下面的内容:
/debug/pprof/
profiles:
0 block
756 goroutine
16100 heap
0 mutex
94 threadcreate
full goroutine stack dump
有了 profile 数据之后(不管是文件还是网络请求),下一步就是要对这些数据进行分析。我们可以使用 go tool pprof
命令行工具。
在后面我们会生成调用关系图和火焰图,需要安装 graphviz
软件包,在 ubuntu 系统可以使用下面的命令:
$ sudo apt-get install -y graphviz
注意获取的 profile 数据是动态的,要想获得有效的数据,请保证应用处于较大的负载(比如正在运行的服务,或者通过其他工具模拟访问压力)。否则如果应用处于空闲状态,得到的结果可能没有任何意义。
我们以 CPU profile 分析为例介绍两种分析方法。
go tool pprof
最简单的使用方式为 go tool pprof [binary] [source]
,binary
是应用的二进制文件,用来解析各种符号;source
表示 profile 数据的来源,可以是本地的文件,也可以是 http 地址。比如:
➜ go tool pprof ./hyperkube http://172.16.3.232:10251/debug/pprof/profile
Fetching profile from http://172.16.3.232:10251/debug/pprof/profile
Please wait... (30s)
Saved profile in /home/cizixs/pprof/pprof.hyperkube.172.16.3.232:10251.samples.cpu.002.pb.gz
Entering interactive mode (type "help" for commands)
(pprof)
这个命令会进行 CPU profile 分析,等待一段时间(默认是 30s,如果在 url 最后加上 ?seconds=60
参数可以调整采集数据的时间为 60s)之后,我们就进入了一个交互式命令行,可以对解析的结果进行查看和导出。可以通过 help
来查看支持的命令有哪些。
一个有用的命令是 topN
,它列出最耗时间的地方:
(pprof) top10
130ms of 360ms total (36.11%)
Showing top 10 nodes out of 180 (cum >= 10ms)
flat flat% sum% cum cum%
20ms 5.56% 5.56% 100ms 27.78% encoding/json.(*decodeState).object
20ms 5.56% 11.11% 20ms 5.56% runtime.(*mspan).refillAllocCache
20ms 5.56% 16.67% 20ms 5.56% runtime.futex
10ms 2.78% 19.44% 10ms 2.78% encoding/json.(*decodeState).literalStore
10ms 2.78% 22.22% 10ms 2.78% encoding/json.(*decodeState).scanWhile
10ms 2.78% 25.00% 40ms 11.11% encoding/json.checkValid
10ms 2.78% 27.78% 10ms 2.78% encoding/json.simpleLetterEqualFold
10ms 2.78% 30.56% 10ms 2.78% encoding/json.stateBeginValue
10ms 2.78% 33.33% 10ms 2.78% encoding/json.stateEndValue
10ms 2.78% 36.11% 10ms 2.78% encoding/json.stateInString
每一行表示一个函数的信息。前两列表示函数在 CPU 上运行的时间以及百分比;第三列是当前所有函数累加使用 CPU 的比例;第四列和第五列代表这个函数以及子函数运行所占用的时间和比例(也被称为累加值 cumulative
),应该大于等于前两列的值;最后一列就是函数的名字。如果应用程序有性能问题,上面这些信息应该能告诉我们时间都花费在哪些函数的执行上了。
pprof 不仅能打印出最耗时的地方(top
),还能列出函数代码以及对应的取样数据、汇编代码以及对应的取样数据。list
命令后面跟着一个正则表达式,就能查看匹配函数的代码以及每行代码的耗时:
(pprof) list podFitsOnNode
Total: 120ms
ROUTINE ======================== k8s.io/kubernetes/plugin/pkg/scheduler.podFitsOnNode in /home/cizixs/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/plugin/pkg/scheduler/generic_scheduler.go
0 20ms (flat, cum) 16.67% of Total
. . 230:
. . 231:// Checks whether node with a given name and NodeInfo satisfies all predicateFuncs.
. . 232:func podFitsOnNode(pod *api.Pod, meta interface{}, info *schedulercache.NodeInfo, predicateFuncs map[string]algorithm.FitPredicate) (bool, []algorithm.PredicateFailureReason, error) {
. . 233: var failedPredicates []algorithm.PredicateFailureReason
. . 234: for _, predicate := range predicateFuncs {
. 20ms 235: fit, reasons, err := predicate(pod, meta, info)
. . 236: if err != nil {
. . 237: err := fmt.Errorf("SchedulerPredicates failed due to %v, which is unexpected.", err)
. . 238: return false, []algorithm.PredicateFailureReason{}, err
. . 239: }
. . 240: if !fit {
如果想要了解对应的汇编代码,可以使用 disadm
命令。
pprof 能以各种样式输出数据,比如 svg、gv、callgrind、png、gif 等等。其中一个非常便利的方法是在交互式终端中输入 web 命令
,就能自动生成一个 svg
文件,并跳转到浏览器打开,生成了一个函数调用图:
这个调用图包含了更多的信息,而且可视化的图像能让我们更清楚地理解整个应用程序的全貌。图中每个方框对应一个函数,方框越大代表执行的时间越久(包括它调用的子函数执行时间,但并不是正比的关系);方框之间的箭头代表着调用关系,箭头上的数字代表被调用函数的执行时间。
因为原图比较大,这里只截取了其中一部分,但是能明显看到 encoding/json.(*decodeState).object
是这里耗时比较多的地方,而且能看到它调用了哪些函数。这些信息对于定位和调优性能是非常有帮助的。如果想进一步在浏览器中查看源代码和汇编代码,可以使用 weblist 命令,和 list、disadm 的用法相同,它
能够同时显示源代码和汇编代码。
此外还可以输入 pdf 命令
生成一个 pdf
文件。更详细的 pprof 使用方法可以参考 pprof --help
或者 pprof 文档。
另一个可视化的方法是直接启动一个 http 服务:
go tool pprof -http="10.224.27.152:8081" ./hyperkube http://172.16.3.232:10251/debug/pprof/profile
在浏览器上访问 10.224.27.152:8081 即可看到各种界面。
注:本文大量使用了 使用 pprof 和火焰图调试 golang 应用 的内容,并结合了笔者平时的实践。