这是我参与「第五届青训营」笔记创作活动的第8天!
本节课学习内容
- 性能调优原则
- pprof工具
- 性能调优案例
性能调优原则
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
pprof工具
pprof 功能简介
pprof是用于可视化和分析性能分析数据的工具
pprof 排查实战
搭建pprof实践项目
将https:l/github.com/wolfogre/go-pprof-practice 开源项目clone到本地运行
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
log.SetOutput(os.Stdout)
runtime.GOMAXPROCS(1)
runtime.SetMutexProfileFraction(1)
runtime.SetBlockProfileRate(1)
go func() {
if err := http.ListenAndServe(":6060", nil); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
for {
for _, v := range animal.AllAnimals {
v.Live()
}
time.Sleep(time.Second)
}
}
运行后在浏览器打开/debug/pprof/查看相应指标
采样数据具体介绍
- allocs:内存分配情况
- blocks:阻塞操作情况
- cmdline:程序启动命令及
- goroutine:当前所有goroutine的堆栈信息
- heap:堆上内存使用情况(同alloc)
- mutex:锁竞争操作情况
- profile: CPU占用情况
- threadcreate:当前所有创建的系统线程的堆栈信息
- trace:程序运行跟踪信息
CPU
运行程序查看CPU使用情况
在终端执行如下命令
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
再输入top命令得到如下结果
具体参数含义:
- flat:当前函数本身的执行消耗
- flat%:flat占CPU总时间的比例
- sum%:上面每一行的flat%的总和
- cum:指当前函数本身加上其调用函数的总耗时
- cum%:cum占CPU总时间的比例
分析:
- 当flat==cum ,函数中没有调用其它函数
- 当flat==0 ,函数中只有其它函数的调用
输入list命令,作用是根据指定的正则表达式查找代码行
发现for循环占用很大的CPU,将tiger中的for循环注释掉会发现CPU占用情况有很大降低
Heap——堆内存
运行程序后在终端输入如下命令
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
等待采样完成后,浏览器会被自动打开,展示出熟悉的web视图,同时展示的资源使用从CPU时间变为了内存占用
Top视图
Source视图
根据源码我们会发现,在*Mouse.Steall这个函数会向固定的Bufer中不断追加1MB内存,直到Buffer达到1GB大小为止,和我们在Graph视图中发现的情况一致。我们将这里的问题代码注释掉,至此,炸弹已被拔除了两个。
goroutine——协程
goroutinue泄露也会导致内存泄露
在终端执行如下命令
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
火焰图是非常常用的性能分析工具,在程序逻辑复杂的情况下很有用,可以重点熟悉。 打开View菜单,切换到Flame Graph视图可以看到,刚才的节点被堆叠了起来
- 由上到下表示调用顺序
- 每一块代表一个函数,越长代表占用CPU的时间更长
- 火焰图是动态的,支持点击块进行分析
图中,自顶向下展示了各个调用,表示各个函数调用之间的层级关系每—行中,条形越长代表消耗的资源占比越多。显然,那些又平又长的节点是占用资源多的节点。可以看到,*Wolf.Drink()这个调用创建了超过90%的goroutine,问题可能在这里
在Source中搜索wolf
发现函数每次会发起10条无意义的goroutine,每条等待30秒后才退出,导致了goroutine的泄露。这里为了模拟泄漏场景,只等待了30秒就退出了试想,如果发起的goroutine没有退出,同时不断有新的goroutine披后动,对应的内存占用持续增长,CPU调度压力也不晰增大,最终进程会被系统杀死
mutex——锁
修改链接后缀,改成mutex,然后打开网页观察,发现存在1个锁操作同样地,在Graph视图中定位到出问题的函数在*Wolf.Howl0
然后在Source视图中定位到具体哪—行发生了锁竞争在这个函数中,goroutine足足等待了几秒才解锁,在这里阻塞住了,显然不是什么业务需求,注释掉。
block——阻塞
与上面相同,不赘述
小结
以上我们介绍了
- 五种使用prof采集的常用性能指标:CPU、堆内存、Goroutine、锁竞争和阻塞
- 两种展示方式交互式终端和网页
- 四种视图:Top、Graph、源码和火焰图
pprof的采样过程和原理
CPU
CPU采样会记录所有的调用栈和它们的占用时间。在采样时,进程会每秒暂停一百次,每次会记录当前的调用栈信息。汇总之后,根据调用栈在采样中出现的次数来推断函数的运行时间。你需要手动地启动和停止采样。每秒100次的暂停频率也不能更改。这个定时暂停机制在unix或类unix系统上是依赖信号机制实现的。每次暂停都会接收到一个信号,通过系统计时器来保证这个信号是固定频率发送的。 具体流程如下:
共有三个相关角色 进程本身、操作系统和写缓冲。 启动采样时,进程向OS注册一个定时器,OS会每隔10ms向进程发送一个SIGPROF信号,进程接收到信号后就会对当前的调用栈进行记录,与此同时,进程会启动一个写缓冲的goroutine,它会每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流。当采样停止时,进程向OS取消定时器,不再接收信号,写缓冲读取不到新的堆栈时,结束输出。
堆内存——Heap
提到内存指标的时候说的都是堆内存而不是内存,这是因为pprof的内存采样是有局限性的。与CPU和goroutine都不同的是,内存的采样是一个持续的过程,它会记录从程序运行起的所有分配或释放的内存大小和对象数量,并在采样时遍历这些结果进行汇总。
协程和线程
Goroutie采样会记录所有用户发起,也就是入口不是runtime开头的goroutine,以及main函数所在goroutine的信息和创建这些goroutine的调用栈。他们在实现上非常的相似,都是会在STW之后,遍历所有goroutine)所有线程的列表(图中的m就是GMP模型中的m,在golang中和线程一一对应)并输出堆栈最后Start The World继续运行。这个采样是立刻触发的全量记录,你可以通过比较两个时间点的差值来得到某—时间段的指标。
阻塞和锁
性能调优案例
业务服务优化
优化流程
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证
基础库优化
Go语言优化
总结
这节课主要学习了性能调优,性能调优的流程很长,这里做下总结。在性能评估中要依靠数据,用实际的结果做决策。对于pprof工具,可以通过分析实际的程序熟悉相关功能,理解基本原理,后续能够更好地解决性能问题在真正的服务性能调优流程中,链路会很长,重点是要保证正确性,不影响功能,同时定位主要问题。