性能分析工具pprof简介
pprof 是 Go 语言内置的性能分析工具,用于捕获和分析程序的性能数据,分为工具、采样、分析、展示四个部分。通过 pprof,开发者可以快速定位程序的性能瓶颈,如 CPU 使用率、内存分配、Goroutine 的使用等问题,从而优化代码。
前期准备
获取“炸弹”代码:
Github项目地址:wolfogre/go-pprof-practice: go pprof practice.
package main
import (
"log"
"net/http"
_ "net/http/pprof" //自动注册 pprof 的 handler 到 http server
"os"
"runtime"
"time"
"github.com/wolfogre/go-pprof-practice/animal"
)
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
log.SetOutput(os.Stdout)
runtime.GOMAXPROCS(1)
runtime.SetMutexProfileFraction(1)
runtime.SetBlockProfileRate(1)
//启动 http server
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)
}
}
机器至少需要:2 核 CPU,2G 内存。然后运行并编译“炸弹”,保证“炸弹”代码能顺利启动。运行后可以看到,“炸弹”程序占用了11%的CPU,3G的内存。
浏览器查看指标
保持程序运行,打开浏览器访问 http://localhost:6060/debug/pprof/,可以看到如下页面:
可以点击查看各个统计部分的具体统计内容
本文将主要对下图中框出几个指标进行排查,分析是否存在性能问题。
开始排查
1.CPU分析
在终端输入下面这条命令,对CPU的运行情况采样10秒,后续我们对这些采样的数据进行分析。
go tool pprof "http://localhost:6060/debug/pprof/profile?second=10"
接着输入
top命令,可以查看占用资源最多的函数。观察图示结果可以发现,Tiger.Eat()占用了最多的CPU资源。
图中各指标的含义:
flat:当前函数本身的执行耗时
flat%:flat占总CPU运行耗时的比例
sum%:上面每一行flat%的总和
cum:指当前函数本身加上其调用函数的总耗时
cum%:cum占总CPU运行耗时的比例
除此之外,我们还能发现,有时flat == cum,有时却是flat == 0,读者可以结合上方给出的各字段的含义,思考一下这两种分别表示什么意思。
flat == cum:函数中没有调用其他函数
flat == 0:函数中只有其他函数的调用
定位到最消耗资源的函数方法后,可以使用命令list来根据指定的正则表达式寻找代码行,例如输入下方这行命令。
list Eat
观察结果可以找到有一个for循环消耗了28.93秒的CPU时间,占用了大量资源。
除了终端页面,还可以输入web命令来调用关系可视化。
我们找到消耗最多资源的代码段后并将注释后,重新运行程序,观察占用资源,能发现CPU资源大幅降低。
但是在上面我们能看到内存占用仍然居高不下,所以接下来我们开始寻找最占用内存的代码在哪,在终端中输入以下指令。
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
之后会打开如下图所示的网页,展示程序的内存占用情况。
点击左上角的VIEW,可以看到有多种可视化的显示方式。
选择Source视图,可以看到第60行的代码占用了1.20G的内存。
同时我们能在右上角看到,此时查看的是inuse_space的占用情况,点击左上角的SAMPLE菜单可以看到有四种占用情况的统计。
alloc_objects:程序累计申请的对象数
alloc_space:程序累计申请的内存大小
inuse_obiects:程序当前持有的对象数
inuse_space:程序当前占用的内存大小
我们切换到alloc_space页面后,能发现还有一处内存的大量占用,此段代码的操作是每次申请一段16M的内存,但可能在申请完之后没有使用,然后马上就被GC回收了,因此一直在申请内存的分配。
我们简单地将上面那些问题代码注释后,重新运行程序,可以看到内存占用也大幅度地下降了。
通过pprof的统计也能看到申请内存的次数也大幅减少了,但是另一个问题出现了,存在着一百个以上的线程,一般的程序肯定不会用到这么多的线程数,我们来看看是怎么回事。
同样地,先在终端输入采样线程情况的命令。
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
能看到线程数在Graph视图下的表示,但这个图的显示内容非常长,不利于观察和分析,所以我们选择另一种视图。
在左上角的VIEM菜单中选择新的视图Flame Graph,即火焰图,就可以看到类似下图的页面。从下图可以发现,在wolf.Drink中有一个time.Sleep操作占用了大量的线程。
火焰图有以下几个特点:
1.由上到下表示调用顺序
2.每一块代表一个函数,越长代表占用 CPU 的时间更长
3.火焰图是动态的,支持点击块进行分析
然后我们就可以在Source视图里搜索wolf来定位问题代码,然后将这行问题代码先简单地注释掉。
重新运行程序,就可以看到goroutine的数量大幅下降了。
接下来我们继续检查阻塞 block 和锁 mutex是否有可以优化的地方。在终端输入采样锁情况的命令。
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
按照我们前几次的操作,打开Souce视图,定位到可以优化的代码段,并简单地注释掉。
再次运行,就可以看到mutex的部分也被我们优化了。
接着优化block的操作就不再赘述,只给出关键步骤,为读者提供一个练手的机会。
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
统计情况为1的原因是http相关操作造成的,可点击
block查看
总结
性能调优原则
1.要依靠数据而不是猜测
使用pprof,主要针对CPU、堆内存-Heap、协程-Goroutine进行采样,在网页上通过Top、Graph、Flame Graph、Source视图分析并定位可以优化的代码。
2.要定位最大瓶颈而不是细枝末节
我们只需要优化对性能影响最大的地方,一些对性能影响不大的代码并不一定需要优化。
3.不要过早优化
因为产品的功能在快速地迭代变化,同时产品服务的用户数量也在不断变化,在预期要出现性能问题时,再进行优化
4.不要过度优化
过度优化可能导致代码运行方式的变化,影响后续的迭代和扩展