青训营后端笔记3- Go 程序性能优化过程与思路 | 豆包MarsCode AI刷题

125 阅读6分钟
性能分析工具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的内存。 image.png

浏览器查看指标

保持程序运行,打开浏览器访问 http://localhost:6060/debug/pprof/,可以看到如下页面: image.png 可以点击查看各个统计部分的具体统计内容 image.png 本文将主要对下图中框出几个指标进行排查,分析是否存在性能问题。 image.png

开始排查

1.CPU分析

在终端输入下面这条命令,对CPU的运行情况采样10秒,后续我们对这些采样的数据进行分析。

go tool pprof "http://localhost:6060/debug/pprof/profile?second=10"

image.png 接着输入top命令,可以查看占用资源最多的函数。观察图示结果可以发现,Tiger.Eat()占用了最多的CPU资源。 image.png

图中各指标的含义:

flat:当前函数本身的执行耗时

flat%:flat占总CPU运行耗时的比例

sum%:上面每一行flat%的总和

cum:指当前函数本身加上其调用函数的总耗时

cum%:cum占总CPU运行耗时的比例

除此之外,我们还能发现,有时flat == cum,有时却是flat == 0,读者可以结合上方给出的各字段的含义,思考一下这两种分别表示什么意思。 image.png

flat == cum:函数中没有调用其他函数

flat == 0:函数中只有其他函数的调用

定位到最消耗资源的函数方法后,可以使用命令list来根据指定的正则表达式寻找代码行,例如输入下方这行命令。

list Eat

观察结果可以找到有一个for循环消耗了28.93秒的CPU时间,占用了大量资源。 image.png

除了终端页面,还可以输入web命令来调用关系可视化。 image.png

我们找到消耗最多资源的代码段后并将注释后,重新运行程序,观察占用资源,能发现CPU资源大幅降低。

image.png

但是在上面我们能看到内存占用仍然居高不下,所以接下来我们开始寻找最占用内存的代码在哪,在终端中输入以下指令。

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"

之后会打开如下图所示的网页,展示程序的内存占用情况。

image.png

点击左上角的VIEW,可以看到有多种可视化的显示方式。

image.png

选择Source视图,可以看到第60行的代码占用了1.20G的内存。

image.png

同时我们能在右上角看到,此时查看的是inuse_space的占用情况,点击左上角的SAMPLE菜单可以看到有四种占用情况的统计。 image.png

alloc_objects:程序累计申请的对象数

alloc_space:程序累计申请的内存大小

inuse_obiects:程序当前持有的对象数

inuse_space:程序当前占用的内存大小

我们切换到alloc_space页面后,能发现还有一处内存的大量占用,此段代码的操作是每次申请一段16M的内存,但可能在申请完之后没有使用,然后马上就被GC回收了,因此一直在申请内存的分配。 image.png

image.png

我们简单地将上面那些问题代码注释后,重新运行程序,可以看到内存占用也大幅度地下降了。

image.png

通过pprof的统计也能看到申请内存的次数也大幅减少了,但是另一个问题出现了,存在着一百个以上的线程,一般的程序肯定不会用到这么多的线程数,我们来看看是怎么回事。

image.png

同样地,先在终端输入采样线程情况的命令。

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"

能看到线程数在Graph视图下的表示,但这个图的显示内容非常长,不利于观察和分析,所以我们选择另一种视图。

image.png

在左上角的VIEM菜单中选择新的视图Flame Graph,即火焰图,就可以看到类似下图的页面。从下图可以发现,在wolf.Drink中有一个time.Sleep操作占用了大量的线程。

image.png

火焰图有以下几个特点:

1.由上到下表示调用顺序

2.每一块代表一个函数,越长代表占用 CPU 的时间更长

3.火焰图是动态的,支持点击块进行分析

然后我们就可以在Source视图里搜索wolf来定位问题代码,然后将这行问题代码先简单地注释掉。

image.png

重新运行程序,就可以看到goroutine的数量大幅下降了。

image.png

接下来我们继续检查阻塞 block 和锁 mutex是否有可以优化的地方。在终端输入采样锁情况的命令。

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex" 

按照我们前几次的操作,打开Souce视图,定位到可以优化的代码段,并简单地注释掉。

image.png

再次运行,就可以看到mutex的部分也被我们优化了。

image.png

接着优化block的操作就不再赘述,只给出关键步骤,为读者提供一个练手的机会。

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"  

image.png

image.png

image.png

image.png

统计情况为1的原因是http相关操作造成的,可点击block查看 image.png

总结

性能调优原则
1.要依靠数据而不是猜测

使用pprof,主要针对CPU、堆内存-Heap、协程-Goroutine进行采样,在网页上通过TopGraphFlame GraphSource视图分析并定位可以优化的代码。

2.要定位最大瓶颈而不是细枝末节

我们只需要优化对性能影响最大的地方,一些对性能影响不大的代码并不一定需要优化。

3.不要过早优化

因为产品的功能在快速地迭代变化,同时产品服务的用户数量也在不断变化,在预期要出现性能问题时,再进行优化

4.不要过度优化

过度优化可能导致代码运行方式的变化,影响后续的迭代和扩展