Golang 是一个对性能特别看重的语言,因此语言中自带了 一些性能分析工具。可以生成相应的Profile(概要文件),譬如CPU Profile、Memory Profile、Block Profile、Mutex Profile、Goroutine Profile等等
CPU Profile:报告程序的 CPU 使用情况,按照一定频率采集应用程序在 CPU 和寄存器上面的数据
Memory Profile(Heap Profile):报告程序的内存使用情况
Block Profile:报告 goroutines 不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈问题
Goroutine Profile:报告 goroutines 的使用情况,有哪些 goroutine,它们的调用关系是怎样的
pprof命令:
usage:
Produce output in the specified format.
pprof [options] [binary]
binary 是应用的二进制文件,用来解析各种符号;source 表示 profile 数据的来源,可以是本地的文件,也可以是 http 地址
在pprof界面,通过 TopN 命令显示重要信息。
如果我们在top命令后加入标签–cum,那么输出的列表就是以累积取样计数为顺序的
当开启CPU Profile时,Go程序每秒钟进行100次采样。
The web command writes a graph of the profile data in SVG format and opens it in a web browser. (There is also a gv command that writes PostScript and opens it in Ghostview. For either command, you need graphviz installed.)
通过 Web 命令以SVG格式创建 Profile数据表,并且在web浏览器中进行展示。
同时,也可以通过gv命令写PostScript,并且在Ghostview中打开。不管哪一个命令,都需要安装graphviz。
在图中看到耗时的操作时,譬如 function1,那么可以通过如下命令仔细观察:
web function1
可以通过如下命令查看源码
list function1
go tool pprof 最简单的使用方式为 go tool pprof [binary] [source],binary 是应用的二进制文件,用来解析各种符号;source 表示 Profile 数据的来源,可以是本地的文件,也可以是 http 地址。
如果程序名为testDemo,profile文件为testDemo.prof,那么通过如下命令就能展示程序的一些运行信息:
go tool pprof testDemo testDemo.prof
那么问题来了,如何生成Profile文件?
根据程序的性质不同,生成profile文件的方式也不同。
对于Web应用,非常简单,如果使用了默认的 http.DefaultServeMux(通常是代码直接使用 http.ListenAndServe(“0.0.0.0:8000”, nil)),在命令源码文件中导入net/http/pprof包就可以,具体形式如下:
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)
方法一
对于普通的应用程序,可以采用和Web应用相同的方法,在main函数中添加如下代码:
import "net/http"
import "log"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
或者
package main
import (
"net/http"
_ "net/http/pprof"
)
func hiHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
}
func main() {
http.HandleFunc("/", hiHandler)
http.ListenAndServe(":8080", nil)
}
**方法二 **
主动将各种概要文件写入文件。
对于CPU Profile,可以采用如下方式,在main函数的入口处调用 WriteCPUProfile 函数:
var cpuProfile = flag.String("cpuProfile", "", "write cpu profile to file")
func WriteCPUProfile() {
if *cpuProfile != "" {
f, err := os.Create(*cpuProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Can not create cpu profile output file: %s",
err)
return
}
if err := pprof.StartCPUProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "Can not start cpu profile: %s", err)
f.Close()
return
}
defer pprof.StopCPUProfile() // 把记录的概要信息写到已指定的文件
}
}
或者
func startCPUProfile() {
if *cpuProfile != "" {
f, err := os.Create(*cpuProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Can not create cpu profile output file: %s",
err)
return
}
if err := pprof.StartCPUProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "Can not start cpu profile: %s", err)
f.Close()
return
}
}
}
停止函数
func stopCPUProfile() {
if *cpuProfile != "" {
pprof.StopCPUProfile() // 把记录的概要信息写到已指定的文件
}
}
调用方法
var cpuProfile = flag.String("cpuprofile", "", "write a cpu profile to the named file during execution")
func main(){
startCPUProfile()
defer stopCPUProfile()
}
-cpuprofile 指定CPU概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。
使用方法:
go run testDemo.go -p="runtime" -cpuprofile="./pprof/cpu.pprof"
-p是对命令源码文件 testDemo.go 有效的。其含义是指定要解析依赖关系的代码包的导入路径。(明确)
对于内存概要函数,分为start和stop两个函数:
var memProfile = flag.String("memprofile", "", "write a memory profile to the named file after execution")
var memProfileRate = flag.Int("memprofilerate", 0, "if > 0, sets runtime.MemProfileRate")
func startMemProfile() {
if *memProfile != "" && *memProfileRate > 0 {
runtime.MemProfileRate = *memProfileRate
}
}
memProfile是内存概要文件的路径。memProfileRate的含义是分析器的取样间隔,单位是字节。如果我们不给runtime.MemProfileRate变量赋值,内存使用情况的取样操作也会照样进行。此取样操作会从用户程序开始时启动,且一直持续进行到用户程序结束。runtime.MemProfileRate变量的默认值是512K字节。只有当我们显式的将0赋给runtime.MemProfileRate变量之后,才会取消取样操作。
在默认情况下,内存使用情况的取样数据只会被保存在运行时内存中,而保存到文件的操作只能由我们自己来完成。
停止函数:
func stopMemProfile() {
if *memProfile != "" {
f, err := os.Create(*memProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Can not create mem profile output file: %s", err)
return
}
if err = pprof.WriteHeapProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "Can not write %s: %s", *memProfile, err)
}
f.Close()
}
}
stopMemProfile函数的功能是停止对内存使用情况的取样操作。但是,它只做了将取样数据保存到内存概要文件的操作。在stopMemProfile函数中,我们调用了函数pprof.WriteHeapProfile,并把代表内存概要文件的文件实例作为了参数。如果pprof.WriteHeapProfile函数没有返回错误,就说明数据已被写入到了内存概要文件中。
-memprofile 指定内存概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。
-memprofilerate 定义其值为n。此标记指定每分配n个字节的堆内存时,进行一次取样操作。
使用方法:
go run testDemo.go -p="runtime" -memprofile="./pprof/mem.pprof" -memprofilerate=10
程序阻塞概要文件用于保存用户程序中的Goroutine阻塞事件的记录。
var blockProfile = flag.String("blockprofile", "", "write a goroutine blocking profile to the named file after execution")
var blockProfileRate = flag.Int("blockprofilerate", 1, "if > 0, calls runtime.SetBlockProfileRate()")
func startBlockProfile() {
if *blockProfile != "" && *blockProfileRate > 0 {
runtime.SetBlockProfileRate(*blockProfileRate)
}
}
blockProfile的含义为程序阻塞概要文件的路径。blockProfileRate的含义是分析器的取样间隔,单位是次。默认取样间隔为1,即默认情况下,每发生一次Goroutine阻塞事件,分析器就会取样一次。与内存使用情况记录一样,运行时系统对Goroutine阻塞事件的取样操作也会贯穿于用户程序的整个运行期。但是,如果我们通过runtime.SetBlockProfileRate函数将这个取样间隔设置为0或者负数,那么这个取样操作就会被取消。
将保存在运行时内存中的Goroutine阻塞事件记录存放到指定的文件,代码如下:
func stopBlockProfile() {
if *blockProfile != "" && *blockProfileRate >= 0 {
f, err := os.Create(*blockProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "Can not create block profile output file: %s", err)
return
}
if err = pprof.Lookup("block").WriteTo(f, 0); err != nil {
fmt.Fprintf(os.Stderr, "Can not write %s: %s", *blockProfile, err)
}
f.Close()
}
}
在创建程序阻塞概要文件之后,stopBlockProfile函数会先通过函数pprof.Lookup将保存在运行时内存中的内存使用情况记录取出,并在记录的实例上调用WriteTo方法将记录写入到文件中。
-blockprofile 指定程序阻塞概要文件的保存路径。该路径可以是相对路径也可以是绝对路径,但其父路径必须已存在。
-blockprofilerate 定义其值为n。此标记指定每发生n次Goroutine阻塞事件时,进行一次取样操作。
使用方法:
go run testDemo.go -p="runtime" -blockprofile="./pprof/block.pprof" -blockprofilerate=10
通过pprof.Lookup函数取出更多种类的取样记录,具体如下:
名称 | 说明 | 取样频率 |
---|---|---|
goroutine | 活跃Goroutine的信息的记录。 | 仅在获取时取样一次。 |
threadcreate | 系统线程创建情况的记录。 | 仅在获取时取样一次。 |
heap | 堆内存分配情况的记录。 | 默认每分配512K字节时取样一次。 |
block | Goroutine阻塞事件的记录。 | 默认每发生一次阻塞事件时取样一次 |
为方便展示效果,先以Web应用展示Pprof的使用,示例程序见Github:
查看堆信息:
go tool pprof http://localhost:9090/debug/pprof/heap
在pprof界面中,输入web命令:
web
查看阻塞信息:
go tool pprof http://localhost:9090/debug/pprof/block
查看锁信息(to look at the holders of contended mutexes, after calling runtime.SetMutexProfileFraction in your program)
go tool pprof http://localhost:9090/debug/pprof/mutex
查看60s的CPU信息
go tool pprof http://localhost:9090/debug/pprof/profile?seconds=60
输入web指令,输出信息:
采集5s内的程序执行路径信息:
wget http://localhost:9090/debug/pprof/trace?seconds=5
如果需要查看所有可用的profile,在浏览器中打开:
http://localhost:9090/debug/pprof/
测试程序如下见Github,只进行cpu性能分析.
通过 go install havlak1.go生成可执行程序havlak1
首先产生prof文件:
havlak1 -cpuprofile=havlak1.prof
其次,对prof文件进行分析:
go tool pprof havlak1 havlak1.prof
在pprof操作界面:
F:\ProgramTest\Golang\goBin>go tool pprof havlak1.exe havlak1.prof
File: havlak1.exe
Type: cpu
Time: Jul 25, 2019 at 6:34am (CST)
Duration: 20.10s, Total samples = 25.87s (128.68%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
输入以下指令查看最占CPU的10个函数:
top 10
结果如下:
Showing nodes accounting for 19400ms, 74.99% of 25870ms total
Dropped 107 nodes (cum <= 129.35ms)
Showing top 10 nodes out of 55
flat flat% sum% cum cum%
3540ms 13.68% 13.68% 3620ms 13.99% runtime.mapaccess1_fast64
3100ms 11.98% 25.67% 6690ms 25.86% runtime.scanobject
2420ms 9.35% 35.02% 12460ms 48.16% main.FindLoops
2070ms 8.00% 43.02% 3410ms 13.18% runtime.mallocgc
2020ms 7.81% 50.83% 2020ms 7.81% runtime.heapBitsForObject
1840ms 7.11% 57.94% 1860ms 7.19% runtime.greyobject
1410ms 5.45% 63.39% 2800ms 10.82% runtime.mapassign_fast64ptr
1330ms 5.14% 68.53% 3530ms 13.65% main.DFS
890ms 3.44% 71.98% 890ms 3.44% runtime.heapBitsSetType
780ms 3.02% 74.99% 780ms 3.02% runtime.memclrNoHeapPointers
top 5 -cum
结果如下:
Showing nodes accounting for 2.50s, 9.66% of 25.87s total
Dropped 107 nodes (cum <= 0.13s)
Showing top 5 nodes out of 55
flat flat% sum% cum cum%
0 0% 0% 12.53s 48.43% main.main
0 0% 0% 12.53s 48.43% runtime.main
0 0% 0% 12.46s 48.16% main.FindHavlakLoops
2.42s 9.35% 9.35% 12.46s 48.16% main.FindLoops
0.08s 0.31% 9.66% 9.06s 35.02% runtime.systemstack
每一行表示一个函数的信息。前两列表示函数在 CPU 上运行的时间以及百分比;第三列是当前所有函数累加使用 CPU 的比例;第四列和第五列代表这个函数以及子函数运行所占用的时间和比例(也被称为累加值 cumulative),应该大于等于前两列的值;最后一列就是函数的名字。如果应用程序有性能问题,上面这些信息应该能告诉我们时间都花费在哪些函数的执行上了。
pprof 不仅能打印出最耗时的地方(top),还能列出函数代码以及对应的取样数据(list)、汇编代码以及对应的取样数据(disasm),而且能以各种样式进行输出,比如 svg、gv、callgrind、png、gif等等。
其中一个非常便利的是 web 命令,在交互模式下输入 web,就能自动生成一个 svg 文件,并跳转到浏览器打开,生成了一个函数调用图(havlak1_web):
web
Web生成的图包含了更多的信息,而且可视化的图像能让我们更清楚地理解整个应用程序的全貌。图中每个方框对应一个函数,方框越大代表执行的时间越久(包括它调用的子函数执行时间,但并不是正比的关系);方框之间的箭头代表着调用关系,箭头上的数字代表被调用函数的执行时间。
web mapaccess1
go test 命令有两个参数和 pprof 相关,它们分别指定生成的 CPU 和 Memory profiling 保存的文件:
-cpuprofile:cpu profiling 数据要保存的文件地址
-memprofile:memory profiling 数据要报文的文件地址
go test -bench=. -cpuprofile=cpu.prof
go test -bench=. -memprofile=mem.prof
1. go tool pprof cpu.prof
2. go tool pprof -http=:8080 cpu.prof
火焰图(Flame Graph)是 Bredan Gregg 创建的一种性能分析图表,因为它的样子近似火而得名。上面的 profiling 结果也转换成火焰图。Uber开源工具 go-torch可以直接读取 golang profiling 数据,并生成一个火焰图的 svg 文件。
使用火焰图之前,需要安装如下工具
go get github.com/google/pprof
需要将产生的pprof可执行程序所在路径添加到环境变量中。
用法
pprof -http=:8080 cpu.prof
git clone https://github.com/brendangregg/FlameGraph.git
将FlameGraph所在路径添加到环境变量中。
export PATH=$PATH:$GOPATH/FlameGraph
graphviz 用于绘图
CentOS:
yum install graphviz
Ubuntu
sudo apt-get install graphviz
安装go-torch用于采集数据,生产火焰图。
go get github.com/uber/go-torch
需要将产生的pprof可执行程序所在路径添加到环境变量中。
go-torch 工具的使用非常简单,没有任何参数的话,它会尝试从 http://localhost:8080/debug/pprof/profile 获取 profiling 数据。它有三个常用的参数可以调整:
-u --url:要访问的 URL,这里只是主机和端口部分
-s --suffix:pprof profile 的路径,默认为 /debug/pprof/profile
-t --seconds:要执行 profiling 的时间长度,默认为 30s
火焰图 svg 文件可以通过浏览器打开,它对于调用图的最优点是它是动态的:可以通过点击每个方块来 zoom in 分析它上面的内容。
火焰图的调用顺序从下到上,每个方块代表一个函数,它上面一层表示这个函数会调用哪些函数,方块的大小代表了占用 CPU 使用的长短,火焰图的配色并没有特殊的意义。
运行程序(监听于9090端口),输入如下指令
go-torch -u http://localhost:9090 -t 30
随后在执行go-torch的目录下会生成torch.svg文件,通过浏览器打开,如下:
点击图中的任意部分,可以看到更详细信息:
备注:
在Windows下安装同样方式操作时,会有如下错误:
could not generate flame graph: fork/exec F:\Pr
ogramTest\Golang\src\github.com\FlameGraph\flamegraph.pl: %1 is not a valid Win32 application.
问题原因是windows不能直接执行pl文件。pl文件是perl文件,于是安装perl,并且将路径添加到环境变量中。依然报错。因为windows直接可执行的是exe文件。可能需要将下载的flamegraph进行编译,然后进行安装。或者直接下载flamegraph可执行程序,并进行安装。这部分后面完善。
Package pprof
package pprof
Profiling Go Programs
Go 大杀器之性能剖析 PProf
go tool pprof
Go的pprof使用
使用 pprof 和火焰图调试 golang 应用
Profiling and optimizing Go web applications
google/pprof
实战Go内存泄露
扫描二维码,关注“清远的梦呓”公众号,手机端查看文章