性能分析
- 从业务目标角度,通常我们的瓶颈出现在业务的通量和时延两个问题上
- 比如一个MySQL数据库,你从其他机器上对它发请求,每秒它能处理10万个请求,这个就是通量性能;每个请求的反应时间是0.5ms,这个就是时延性能
- 通量和时延是相互相承的两个量,当通量达到系统上限,时延就会大幅提高
- 一般而言,性能分析主要关注 CPU、内存、磁盘 IO、网络这些指标
pprof
三种采样方式
runtime/pprof
-
适用于程序只运行一次的,例如每天只跑一次的离线预处理程序
-
采样
- 运行程序后生成
cpu.prof
和mem.prof
两个文件
- 运行程序后生成
import "runtime/pprof"
func main() {
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
ff, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(ff)
// 待测试的程序
}
net/http/pprof
- 适用于程序长期运行的线上服务
- 底层也是调用的
runtime/pprof
提供的函数,封装成接口对外提供网络访问
- 底层也是调用的
- 如果是使用了HTTP包的路由,则只需要
import _ "net/http/pprof"
即可,会自动注册路由 - 如果使用了第三方路由(例如gin),则需要手动注册路由
- gin还可以直接使用
github.com/DeanThompson/ginpprof
包 - 只需要
ginpprof.Wrap(router)
即可
- gin还可以直接使用
import _ "net/http/pprof"
// 如果不是使用默认的 http.DefaultServeMux而是自定义的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)
benchmark pprof
- 命令行
- 直接运行
go test -bench <bench_func> -run none -cpuprofile <cpu.prof>
- 除了
-cpuprofile
, 还可-memprofile
等
- 直接运行
- Goland
报告查看
# 命令行交互模式
go tool pprof http://<address:port>/debug/pprof/<profile>
# cpu profile默认采样30s,可以自行修改
go tool pprof http://<address:port>/debug/pprof/<profile>?seconds=120
# 在浏览器汇总交互
go tool pprof --http=:<port> http://<address:port>/debug/pprof/<profile>
```
<profile>
包括profile(CPU), heap(内存),goroutine,block,mutex
命令行
-
总的思路就是通过
top
和web
找出关键函数,再通过list Func
查看函数代码,找到关键代码行并确认优化方案 -
top <num>
查看占用最高的num项
-
list <func name>
查看匹配的具体函数CPU和内存等占用数据- 支持正则匹配
- 如果函数名不明确,会进行模糊匹配
- 使用list命令的前提是程序的源码在当前机器
-
traces
打印所有调用栈,以及调用栈的指标信息- 每个
- - - - -
隔开的是一个调用栈
- 每个
浏览器
- 需要先安装
graphviz
工具
指标
CPU
- CPU 性能分析启用后,Go runtime 会每 10ms 就暂停一下,记录当前运行的 goroutine 的调用堆栈及相关数据
内存
-
内存性能分析则是在一段时间内对在堆分配进行采样
- 栈分配由于会随时释放,因此不会被内存分析所记录
- 内存profiling是基于抽样和它跟踪的是已分配的内存,而不是使用中的内存
- 比如有些内存已经分配,但实际以及不使用的内存,比如内存泄露的那部分
- 所以不能使用内存profiling衡量程序总体的内存使用情况
-
有两种内存分析策略:
- 一种是当前的(这一次采集)内存或对象的分配,称为
inuse
- 另一种是从程序运行到现在所有的内存分配,不管是否已经被 gc 过了,称为
alloc
- 加上
-sample_index
参数后,可以切换内存分析的类型 - 例如
go tool pprof -sample_index=alloc_space http://<address:port>/debug/pprof/heap
- 一种是当前的(这一次采集)内存或对象的分配,称为
-inuse_space 未释放空间数
-inuse_objects 未释放对象数
-alloc_space 累计分配空间数
-alloc_objects 累计分配对象数
goroutine
goroutine profile: total 32023
32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 0x6d8559 0x6d831b 0x45abe1
# 0x6d8558 main.alloc2.func1+0xf8 /home/ubuntu/heap/leak_demo.go:53
# 0x6d831a main.alloc2+0x2a /home/ubuntu/heap/leak_demo.go:54
goroutine?debug=1
goroutine profile: total 32023
- 32023是goroutine的总数量
32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 ...
- 32015代表当前有32015个goroutine运行这个调用栈,并且停在相同位置
# 0x6d8558 main.alloc2.func1+0xf8 /home/ubuntu/heap/leak_demo.go:53
- goroutine的调用栈,列出了函数和所在文件的行数
goroutine 20 [chan send, 2 minutes]:
main.alloc2.func1(0xc42015e060)
/home/ubuntu/heap/leak_demo.go:53 +0xf9
main.alloc2(0xc42015e060)
/home/ubuntu/heap/leak_demo.go:54 +0x2b
created by main.alloc1
/home/ubuntu/heap/leak_demo.go:42 +0x3f
goroutine?debug=2
- 和第1种方式是互补的,可以看到每个goroutine的信息
goroutine 20 [chan send, 2 minutes]
- 20是goroutine的id,
[]
中是当前goroutine的状态: 阻塞在写channel,并且阻塞了2分钟
火焰图
- y轴表示调用方法的先后
- x轴表示在每个采样调用时间内,方法所占的时间/空间百分比
- 越宽代表占据cpu时间/mem大小越多
参考
- https://segmentfault.com/a/1190000019222661
- https://juejin.im/entry/5ac9cf3a518825556534c76e
- http://xiaorui.cc/archives/5577
- https://zhuanlan.zhihu.com/p/91241270