Go性能优化工具及调优实战 | 青训营笔记

118 阅读7分钟

这是我参与「第五届青训营」笔记创作活动的第8天!

本节课学习内容

  • 性能调优原则
  • pprof工具
  • 性能调优案例

性能调优原则

  • 要依靠数据不是猜测
  • 要定位最大瓶颈而不是细枝末节
  • 不要过早优化
  • 不要过度优化

pprof工具

pprof 功能简介

pprof是用于可视化和分析性能分析数据的工具 image.png

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/查看相应指标

image.png 采样数据具体介绍

  • allocs:内存分配情况
  • blocks:阻塞操作情况
  • cmdline:程序启动命令及
  • goroutine:当前所有goroutine的堆栈信息
  • heap:堆上内存使用情况(同alloc)
  • mutex:锁竞争操作情况
  • profile: CPU占用情况
  • threadcreate:当前所有创建的系统线程的堆栈信息
  • trace:程序运行跟踪信息

CPU

运行程序查看CPU使用情况

image.png 在终端执行如下命令 go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"

image.png 再输入top命令得到如下结果

image.png 具体参数含义:

  • flat:当前函数本身的执行消耗
  • flat%:flat占CPU总时间的比例
  • sum%:上面每一行的flat%的总和
  • cum:指当前函数本身加上其调用函数的总耗时
  • cum%:cum占CPU总时间的比例

分析:

  • 当flat==cum ,函数中没有调用其它函数
  • 当flat==0 ,函数中只有其它函数的调用

输入list命令,作用是根据指定的正则表达式查找代码行

image.png 发现for循环占用很大的CPU,将tiger中的for循环注释掉会发现CPU占用情况有很大降低

Heap——堆内存 运行程序后在终端输入如下命令 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap" image.png 等待采样完成后,浏览器会被自动打开,展示出熟悉的web视图,同时展示的资源使用从CPU时间变为了内存占用

image.png

Top视图

image.png Source视图

根据源码我们会发现,在*Mouse.Steall这个函数会向固定的Bufer中不断追加1MB内存,直到Buffer达到1GB大小为止,和我们在Graph视图中发现的情况一致。我们将这里的问题代码注释掉,至此,炸弹已被拔除了两个。 image.png

goroutine——协程

goroutinue泄露也会导致内存泄露

在终端执行如下命令 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"

火焰图是非常常用的性能分析工具,在程序逻辑复杂的情况下很有用,可以重点熟悉。 打开View菜单,切换到Flame Graph视图可以看到,刚才的节点被堆叠了起来

  • 由上到下表示调用顺序
  • 每一块代表一个函数,越长代表占用CPU的时间更长
  • 火焰图是动态的,支持点击块进行分析 image.png 图中,自顶向下展示了各个调用,表示各个函数调用之间的层级关系每—行中,条形越长代表消耗的资源占比越多。显然,那些又平又长的节点是占用资源多的节点。可以看到,*Wolf.Drink()这个调用创建了超过90%的goroutine,问题可能在这里

在Source中搜索wolf

image.png发现函数每次会发起10条无意义的goroutine,每条等待30秒后才退出,导致了goroutine的泄露。这里为了模拟泄漏场景,只等待了30秒就退出了试想,如果发起的goroutine没有退出,同时不断有新的goroutine披后动,对应的内存占用持续增长,CPU调度压力也不晰增大,最终进程会被系统杀死

mutex——锁

修改链接后缀,改成mutex,然后打开网页观察,发现存在1个锁操作同样地,在Graph视图中定位到出问题的函数在*Wolf.Howl0 image.png 然后在Source视图中定位到具体哪—行发生了锁竞争在这个函数中,goroutine足足等待了几秒才解锁,在这里阻塞住了,显然不是什么业务需求,注释掉。 image.png

block——阻塞 与上面相同,不赘述 image.png

小结

以上我们介绍了

  • 五种使用prof采集的常用性能指标:CPU、堆内存、Goroutine、锁竞争和阻塞
  • 两种展示方式交互式终端和网页
  • 四种视图:Top、Graph、源码和火焰图 image.png

pprof的采样过程和原理

CPU

CPU采样会记录所有的调用栈和它们的占用时间。在采样时,进程会每秒暂停一百次,每次会记录当前的调用栈信息。汇总之后,根据调用栈在采样中出现的次数来推断函数的运行时间。你需要手动地启动和停止采样。每秒100次的暂停频率也不能更改。这个定时暂停机制在unix或类unix系统上是依赖信号机制实现的。每次暂停都会接收到一个信号,通过系统计时器来保证这个信号是固定频率发送的。 具体流程如下:

image.png

共有三个相关角色 进程本身、操作系统和写缓冲。 启动采样时,进程向OS注册一个定时器,OS会每隔10ms向进程发送一个SIGPROF信号,进程接收到信号后就会对当前的调用栈进行记录,与此同时,进程会启动一个写缓冲的goroutine,它会每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流。当采样停止时,进程向OS取消定时器,不再接收信号,写缓冲读取不到新的堆栈时,结束输出。

image.png

堆内存——Heap

提到内存指标的时候说的都是堆内存而不是内存,这是因为pprof的内存采样是有局限性的。与CPU和goroutine都不同的是,内存的采样是一个持续的过程,它会记录从程序运行起的所有分配或释放的内存大小和对象数量,并在采样时遍历这些结果进行汇总。 image.png

协程和线程

Goroutie采样会记录所有用户发起,也就是入口不是runtime开头的goroutine,以及main函数所在goroutine的信息和创建这些goroutine的调用栈。他们在实现上非常的相似,都是会在STW之后,遍历所有goroutine)所有线程的列表(图中的m就是GMP模型中的m,在golang中和线程一一对应)并输出堆栈最后Start The World继续运行。这个采样是立刻触发的全量记录,你可以通过比较两个时间点的差值来得到某—时间段的指标。 image.png

阻塞和锁

image.png

性能调优案例

业务服务优化

image.png

优化流程

  • 建立服务性能评估手段
  • 分析性能数据,定位性能瓶颈
  • 重点优化项改造
  • 优化效果验证

基础库优化

image.png

Go语言优化

image.png

总结

这节课主要学习了性能调优,性能调优的流程很长,这里做下总结。在性能评估中要依靠数据,用实际的结果做决策。对于pprof工具,可以通过分析实际的程序熟悉相关功能,理解基本原理,后续能够更好地解决性能问题在真正的服务性能调优流程中,链路会很长,重点是要保证正确性,不影响功能,同时定位主要问题。