此篇介绍一下对Go程序进行性能分析的常用工具及用法。

常用命令和工具

pprof
go tool [xxx]
go test
delve
go race
gdb

程序编译时的参数传递(gcflags和ldflags)

gcflags

go build -gcflags '-N -m -l' main.go //可使用go tool compile --help查看可用参数及含义。
比如-N禁用编译优化,-l禁止内联,-m打印编译优化策略(包括逃逸情况和函数是否内联,以及变量分配在堆或栈),-S是打印汇编。

如果只在编译特定包时需要传递参数,格式应遵守“包名=参数列表”,如go build -gcflags='log=-N -l' main.go

开启逃逸分析日志很简单,只要在编译的时候加上-gcflags '-m',另外为了不让编译时自动内联函数,一般也会加-l参数,最终成为-gcflags '-m -l'。即执行如下命令:
$ go build -gcflags '-m -l' main.go

ldflags

go build用 -ldflags 给go链接器传入参数,实际是给go tool link的参数,可以用go tool link --help查看可用的参数。

常用 -X 来指定版本号等编译时才决定的参数值。例如代码中定义var buildVer string,然后在编译时用go build -ldflags "-X main.buildVer=1.0" 来赋值。注意 -X 只能给string类型变量赋值。

go build -x

列出了go build触发的所有命令。
比如工具链、跨平台编译、传入外部编译器的flags、链接器等,可使用 -x 来查看所有的触发。

竞争检测

使用

go run -race main.go

go build -race main.go
来进行竞争检测。

GC日志

  1. 执行前添加系统环境变量 GODEBUG='gctrace=1' 来跟踪打印垃圾回收器信息
  2. 在代码中使用 runtime.ReadMemStats 来获取程序当前内存的使用情况
  3. 使用pprof工具
GODEBUG=gctrace=1 go run main.go  //跟踪打印垃圾回收器信息

Go程序会每隔一段时间打印一些gc信息。

Pprof

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

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

工具型应用分析

CPU分析

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

内存分析

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

使用net/http包时启用Pprof

package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
)

func main() {
	//... do something

	log.Println(http.ListenAndServe("localhost:8090", nil))
}

使用Gin框架时启用Pprof

package main

import (
	"github.com/gin-contrib/pprof"
	"github.com/gin-gonic/gin"
)

func main() {
	app := gin.Default()

	pprof.Register(app)

	app.Run(":8090")
}

访问web页获取分析结果(http://127.0.0.1:8090/debug/pprof,适合http服务)

/debug/pprof/

Types of profiles available:
Count	Profile
3	allocs
0	block    //goroutine的阻塞信息
0	cmdline
4	goroutine    //此项可排查是否创建了大量的 goroutine
3	heap    //堆内存的分配信息
0	mutex    //锁的信息
0	profile
7	threadcreate    //线程信息
0	trace
full goroutine stack dump  //此项可排查是否有 goroutine 运行时间过长
Profile Descriptions:

allocs: A sampling of all past memory allocations
block: Stack traces that led to blocking on synchronization primitives
cmdline: The command line invocation of the current program
goroutine: Stack traces of all current goroutines
heap: A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.
mutex: Stack traces of holders of contended mutexes
profile: CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.
threadcreate: Stack traces that led to the creation of new OS threads
trace: A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.

pprof 支持四种类型的分析

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

CPU Profile:CPU 分析,采样消耗 cpu 的调用,这个一般用来定位排查程序里耗费计算资源的地方

Memory Profile(Heap Profile):内存分析,一般用来排查内存占用,内存泄露等问题。堆内存分配情况的记录。默认每分配512K字节时取样一次。

Block Profile:阻塞分析,报告导致阻塞的同步原语的情况,可以用来分析和查找锁的性能瓶颈。Goroutine阻塞事件的堆栈跟踪记录。默认每发生一次阻塞事件时取样一次。

Goroutine Profile :报告goroutines的使用情况,有哪些goroutine,它们的调用关系是怎样的。活跃Goroutine的信息的记录,以及调用关系。仅在获取时取样一次。

Pprof可视化

在Mac上安装graphviz执行以下命令

brew install graphviz

以本地文件形式获取分析结果

先把信息 dump 到本地文件,然后用 go tool 去分析(生产环境通用的方式)

第三方工具:debugcharts

一个可以实时查看go程序内存、CPU、GC、协程等变化情况的可视化工具。启用方式跟pprof类似,都是先import引入,然后开启端口监听即可。

package main

import (
	_ "github.com/mkevac/debugcharts"
	"log"
	"net/http"
)

func main() {
	//... do something

	log.Println(http.ListenAndServe("localhost:8090", nil))
}

然后在浏览器中打开查看:

http://127.0.0.1:8090/debug/charts/

第三方工具:prometheus

prometheus是grafana的插件,支持go监控的可视化。
启用方式先引入包:

_ "github.com/prometheus/client_golang/prometheus/promhttp"

然后增加路由:

//prometheus
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8090", nil)

最后,通过访问 http://127.0.0.1:8090/metrics 查看采集到的指标数据。

** 可以通过一个端口同时开启pprof + charts + prometheus **

go tool [xxx]

输入 go tool 查看内置的所有[xxx]工具命令

addr2line
api
asm
buildid
cgo
compile:代码汇编
cover:生成代码覆盖率
dist
doc
fix
link
nm:查看符号表(等同于系统 nm 命令)
objdump:反汇编工具,分析二进制文件(等同于系统 objdump 命令)
oldlink
pack
pprof:性能和指标分析工具
test2json
trace:采样一段时间,指标跟踪分析工具
vet

go tool nm 查看符号表的命令

在断点的时候,如果不知道断点的函数符号,可以用这个命令进行查询(命令处理的是二进制程序文件)。

go tool nm ./main
输出的第一列是地址,第二列是类型,第三列是符号。

 115aa00 T bufio.(*ReadWriter).Available
 115aa20 T bufio.(*ReadWriter).Discard
 115aa60 T bufio.(*ReadWriter).Flush
 115aa80 T bufio.(*ReadWriter).Peek

go tool compile 汇编某个文件

go tool compile -N -l -S main.go

go tool objdump 反汇编二进制的工具

go tool objdump main.o
go tool objdump -s DoFunc main.o //反汇编具体函数

go tool pprof 性能指标分析工具

命令行分析模式

go tool pprof http://localhost:8090/debug/pprof/heap?second=10 分析heap,进入命令行模式,输入 web 即可以web方式打开(前提是安装了graphviz)。或者继续输入命令:
在命令行输入top默认查看程序中占用内存前10位的函数
在命令行输入top 3可以查看程序中占用内存前3位的函数
同样,如果采集的是cpu使用top命令可以看占用cpu的函数

输入top后显示的最后一列为函数名称,其他各项内容意义如下:

flat:当前函数占用CPU的耗时
flat%:当前函数占用CPU的耗时百分比
sum%:函数占用CPU的累积耗时百分比
cum:当前函数+调用的子函数 占用CPU总耗时
cum%:当前函数+调用的子函数 占用CPU总耗时百分比

可以在命令行输入 list+函数名 命令查看具体的函数分析
可以在命令行输入 pdf 生成可视化的pdf文件
可以在命令行输入 help 提供所有pprof支持的命令说明

web分析模式

go tool pprof -http=:1234 http://localhost:8090/debug/pprof/heap?second=10 会直接以web方式打开。或者:
http://localhost:1234/ui/ 也可以直接打开,从中可以直接筛选查看火焰图(Flame Graph)。
-http 表示使用交互式web接口查看获取的性能信息,指定可用的端口即可。
debug/pprof/需要查看的指标(allocs,block,goroutine,heap等)
火焰图从上往下是方法的调用栈,长度代表使用的cpu时长。

其他的一些go tool pprof 分析命令

go tool pprof -http=:1234 http://localhost:8090/debug/pprof/goroutine?second=10
go tool pprof --seconds 10 http://localhost:8090/debug/pprof/goroutine

如果应用比较复杂,生成的调用图特别大,看起来很乱,有两个办法可以优化:
使用 web [funcName] 的方式,只打印和某个函数相关的内容
运行 go tool pprof 命令时加上 --nodefration 参数,可以忽略内存使用较少的函数,比如--nodefration=0.05表示如果调用的子函数使用的 CPU、memory 不超过 5%,就忽略它,不要显示在图片中

go test 单元测试

_test.go 结尾的文件认为是测试文件
本质上,golang 跑单测是先编译 *_test.go 文件,编译成二进制后,再运行这个二进制文件

go test .    //直接在本目录中运行go test
go test -run=TestPutAndGetKeyValue    //指定运行函数
go test -v    //提供详细的测试输出,打印测试名称、状态(通过或者失败)、耗时、测试用例的日志等

go test -race    //测试时支持对竞争进行检测和报告
go test -coverprofile=c.out && go tool cover -html=c.out    //输出一个覆盖信息结果并可在浏览器上可视化观看

编译生成单元测试可执行文件

// 先编译出 .test 文件
$ go test -c 
// 指定跑某一个文件
$ ./raftexample.test -test.timeout=10m0s -test.v=true -test.run=TestPutAndGetKeyValue

统计代码覆盖率

  1. 加一个 -coverprofile 的参数,声明在跑单测的时候,记录代码覆盖率

go test -coverprofile=coverage.out

  1. 使用 go tool cover 命令分析,得出覆盖率报告

go tool cover -func=coverage.out

Delve

delve 当前是最友好的 golang 调试程序,ide 调试其实也是调用 dlv 而已,比如 goland
安装dlv:
go get -u github.com/go-delve/delve/cmd/dlv
检查安装版本信息:
dlv version

把程序加载进 Delve 调试器的两种方式(事先需要有go.mod)

加载源码进行调试

dlv debug

  1. 执行 dlv debug 进入命令行模式,此时同目录下会自动生成一个 __debug_bin 文件。这个文件是由源码编译生成的,并会自动加载进调试器。
  2. Delve 期望的是从单个程序或项目中构建出单个二进制文件,如果目录中有多个源文件且每个文件都有自己的主函数, Delve 则可能抛出错误。此种情况下应该使用下面第二种方式,加载二进制文件进行调试。

加载二进制文件进行调试

dlv exec ./main

  1. 使用 dlv exec 命令将二进制文件加载进调试器。
  2. 在命令行模式下输入 help 查看可用命令。
  3. 常使用的一些命令:
b main.main     //在 main 函数处设置断点,等同于 break main.main
b func.go:5     //使用 文件名:行号 的格式来设置断点,也可以直接用行号设置断点
bp                  //查看设置的断点,等同于 breakpoints
clear [断点标号如2]          //清除单个断点
clearall            //清除所有断点
on                  //设置一段命令,当断点命中的时候
c                   //继续运行程序,运行到断点处中止,等同于 continue
n                   //单步调试下一行源码,等同于 next。默认情况下,Delve不会更深入地调试函数调用。
s                   //单步调试下一个函数,等同于 step
step-instruction    //单步调试某个汇编指令
stack              //打印当前堆栈的内容信息,可以看到0、1、2、3...等栈位置的函数
frame 0           //实现帧之间的跳转,可以使用 stack 输出的位置序号
args                //打印出命令行传给函数的参数
disassemble    //查看编译器生成的汇编语言指令
stepout           //跳回到函数被调用的地方
print [var_name]         //打印变量的值
whatis [var_name]        //打印变量的类型
locals                //打印函数内的所有局部变量
regs                    //打印寄存器的信息
x                       //等同于examinemem,这个是解析内存用的,和 gdb 的 x 命令一样
set                     //set赋值
vars                    //打印全局变量(包变量)
whatis                  //打印类型信息
r                       //重新启动并调试执行程序,等同于restart
call                    //整个程序执行
quit                //退出调试器

协程相关
goroutine (alias: gr)       //打印某个特定协程的信息
goroutines (alias: grs)     //列举所有的协程
goroutines -t                   //展开所有协程详细信息
thread (alias: tr)              //切换到某个线程
threads                         //打印所有的线程信息

栈相关
deferred                    //在 defer 函数上下文里执行命令
down                        //上堆栈
frame                       //跳到某个具体的堆栈
stack (alias: bt)          //打印堆栈信息
up                           //下堆栈

其他命令
config                      //配置变更
disassemble (alias: disass)     //反汇编
funcs                           //打印所有函数符号
libraries                       //打印所有加载的动态库
list (alias: ls | l)            //显示源码
source                      //加载命令
sources                     //打印源码
types                       //打印所有类型信息

dlv的其它命令

dlv debug:使用dlv debug可以在main函数文件所在目录直接对main函数进行调试,也可以在根目录以指定包路径的方式对main函数进行调试
dlv exec:使用dlv exec可以对编译好的二进制进行调试
dlv test:使用dlv test可以对test包进行调试
dlv attach:使用dlv attach可以附加到一个已在运行的进程进行调试
dlv connect:使用dlv connect可以连接到调试服务器进行调试
dlv trace:使用dlv trace可以追踪程序

go race

  1. Go 语言提供了 race 检测(Go race detector)来进行竞争分析和发现
  2. go run -race main.go 是运行时检测,并不是编译时。且使用 race 时存在明显的性能开销,因此不要在生产环境中使用这个。

GDB

gdb当前只支持6个命令

  1. 3个 cmd 命令
info goroutines         //打印所有的goroutines
goroutine ${id} bt      //打印一个goroutine的堆栈
iface                       //打印静态或者动态的接口类型
  1. 3个函数
len                         //打印string,slices,map,channels 这四种类型的长度
cap                         //打印slices,channels 这两种类型的cap
dtype                       //强制转换接口到动态类型。

gdb 有一个功能是无法替代的,就是 gcore 的功能

GOGC 环境变量

  1. Go垃圾回收提供了一个参数GOGC。
  2. GOGC代表了占用中的内存增长比率,达到该比率时应当触发1次GC,该参数可以通过环境变量进行设置。。
  3. GOGC参数取值范围为0~100,其默认值是100,单位是百分比。
    假如当前heap占用内存为4MB,GOGC = 75,则 4*(1+75%)=7MB,即heap占用内存大小达到7MB时会触发1轮GC。
  4. GOGC还有2个特殊值:
    “off” : 代表关闭GC
    0 : 代表持续进行垃圾回收,只用于调试。