这是青训营笔记的第五篇文章。
前言
本文主要内容为Go性能调优、性能分析工具------pprof的介绍、pprof的采样过程以及原理解析。
简介
在实际项目中,我们都知道优化很重要,在工作中伴随着迭代的开发,代码的优化要适度进行不要过度,下面是优化的原则可供参考。
性能优化、调优原则:
- 要依靠数据而不是猜测
- 要定位最大瓶颈而不是细枝末叶
- 不要过早优化
- 不要过度优化
性能分析工具
我们希望知道应用在什么地方耗费了多少CPU
、Memory
,有没有什么工具可以检测?
pprof
就是这样的工具,pprof
可用于可视化和分析性能分析数据的工具,下面将对pprof进行介绍以及简单的使用介绍。
pprof
是一个强大的性能分析工具,可以捕捉到多维度的运行状态的数据,Go
在语言层面集成了profile
采样工具,在程序运行过程中可以获取cpu
、heap
、block
、traces
等执行信息,这些会涉及到runtime/pprof
、net/http/pprof
等包,获取profile
数据最有两种形式:web
形式与profile
文件生成形式。
pprof实战
项目搭建
搭建pprof
实践项目,这里使用的是github
上一个开源项目,这个项目提前埋入了一些炸弹代码,产生可观测的性能问题,运行该项目会占用1CPU
核心和超过1GB
的内存。
开源项目地址:github.com/wolfogre/go...
将项目clone下来之后,在终端输入go run main.go
即可运行项目。
指标查看
打开浏览器,输入地址 http://localhost:6060/debug/pprof 即可,如图。
从页面中可以查到,内存、block、协程、堆等分配的情况以及锁的操作等。
CPU
首先看一下CPU的运行情况,打开电脑自带的活动管理器,可以看到此项目在我的电脑上占了8.6%
的CPU。
pprof在每运行一段时间后,就会把运行信息输出到文件中,我们可以通过命令来获取文件。
在终端中输入命令,获取文件:
go
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
topN
命令:top
用于查看占用资源最多的函数
参数说明:
flat
: 当前函数本身的执行耗时flat%
:flat
占CPU
总时间的比例sum%
: 上面每一行的flat%
总和cum
: 当前函数本身加上其周期函数的总耗时cum%
:cum
占CPU
总时间的比例
从图中可以看到,Flat
列有的是0
,有的不是0
,那么Flat
在什么情况下等于Cum
?什么情况下等于0
呢?
根据上述参数说明,可以知道:
- Flat == Cum,说明函数中没有调用其他函数
- Flat == 0,说明函数中只有其他函数的调用
通过top
命令,得出Eat
函数是最消耗CPU
的,下面介绍如何定位到代码的具体行。
list
命令:list
可以根据指定的正则表达式查找代码行
输入命令list Eat
,从输出结果里可以看到对应的文件为tiger.go
,而且具体的代码行为24
行的for
循环。
web
有时候根据命令行查看结果不是很直观,pprof还提供了可视化的查看方式。
命令: web
将调用关系生成可视化文件,可以输出可视化调用量以及每个节点的资源占用数据。
找到代码进行更改,将循环次数改小一些,再次启动项目,查看资源占用情况,可以发现CPU已经没有那么高了。
Heap
经过代码的改造,可以发现CPU的问题已经解决了,但是内存使用还是很高。
这次直接以可视化的方法查看,输入以下命令,打开堆内存的占用情况:
ini
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
打开之后,会出现一下界面:
从上图我们一眼就可以看出,Mouse
这个对象的Steal
方法占用的内存最多。
打开View
->Source
视图,还可以看到具体的代码文件以及对应的行数。
找到对应的代码,将代码进行注释,然后看下资源占用情况。
注释掉mouse.go
文件的第50
行。
重新运行项目,查看资源占用已经很低了。
采样说明,SAMPLE
菜单中有几个选项,对应说明如下:
- alloc_objects: 程序累计申请的对象数
- alloc_space: 程序累计申请的内存大小
- inuse_objects: 程序当前持有的对象数
- inuse_space: 程序当前占用的内存大小
协程
goroutine
泄露也会导致内存泄露
以可视化的方法查看,输入以下命令,打开堆内存的占用情况:
ini
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
火焰图说明:
- 由上到下标识调用顺序
- 每一块代表一个函数,越长代表占用CPU时间更长
- 火焰图是动态的,支持点击块进行分析
切换到Source视图,查看代码具体位置。
可以看出wolf.go
文件里的第34
行,协程里进行了睡眠30s
,修改此处代码即可修复该问题。
mutex
通过pprof
查看锁相关问题,比如加锁时间长等。
执行命令:
ini
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
输出界面如下:
切换到代码视图:
可以发现,wolf.go文件里在释放锁前,进行了睡眠操作,对此处代码进行修改即可修复问题。
block
检查代码中是否存在阻塞,执行命令:
ini
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
输出界面如下:
切换到Source
视图,可以看到cat.go
文件里存在阻塞行为:
同样的,通过top
命令也可以看到存在问题的文件。
pprof原理
下面介绍一下pprof的采样过程以及实现原理。
CPU
采样对象:函数调用和他们占用的时间
采样率:100次/秒,固定值
采样时间:从手动启动到手动结束
采样详细流程如下:
流程说明:
- 操作系统:每10ms向进程发送一次
SIGPROF
信号 - 进程:每次接收到
SIGPROF
信号会记录调用堆栈 - 写缓冲:每100ms读取已经记录的调用栈并写入输出流
堆内存
堆内存采样过程如下:
- 采样程序通过内存分配器在堆上分配和释放内存,记录分配/释放的大小和数量
- 采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
- 采样时间:从程序运行开始到采样时
- 采样指标:alloc_space, alloc_objects, inuse_space, inuse_objects
- 计算方式:inuse = alloc - free
协程&线程创建
协程和线程创建在实现上非常的相似,都是会在STW之后,遍历所有goroutine/所有线程的列表并输出堆栈,最后Start The World继续运行。这个采样是立刻触发的全量记录,可以通过比较两个时间点的差值来得到某一时间段的指标。
协程(Goroutine)
记录所有用户发起且在运行中的goroutine(即入口非runtime开头的)runtime.main的调用栈信息
线程创建(ThreadCreate)
记录程序创建的所有系统线程的信息
协程和线程创建的采样流程如下:
Block&Mutex
阻塞和锁竞争这两个指标在流程和原理也非常相似,这两个采样记录的都是对应操作发生的调用栈、次数和耗时,不过这两个指标的采样率含义并不相同。
阻塞操作的采样率是一个"阈值",消耗超过阈值时间的阻塞操作才会被记录,1为每次操作都会记录。
锁竞争的采样率是一个"比例",运行时会通过随机数来只记录固定比例的锁操作,1为每次操作都会记录。
阻塞操作的采样操作:
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超过阈值的才会被记录,1为每次阻塞均记录
锁竞争的采样操作:
- 采样争抢锁的次数和耗时
- 采样率:只记录固定比例的锁操作,1为每次加锁均记录
阻塞和锁竞争的采样流程如下:
总结
本文主要内容为Go性能调优、性能分析工具------pprof
,pprof
提供了命令行、可视化等排查问题手段,本文通过代码实际运行排查代码中存在的性能问题,通过pprof
可以方便快捷的排查代码中出现的CPU
、内存、堆栈、协程、block
、锁等各个方面的问题。
虽然例子比较简单,但是通过这个例子可以使我们快速上手pprof
工具的使用方法,为之后排查问题、性能优化奠定了良好的基础。