本讲介绍 Go 程序性能分析的技能。
在业务需求初期,其体量往往比较小,即使做了性能优化,收益却并不大。随着业务量的上升,情况随之发生变化。哪怕某个重量级的业务需要几十上百台服务器支撑,哪怕性能优化只提升约 20% 的效益。收益不仅是节省下来的运行时间,更是硬件成本的投入和运行稳定性的保证。这足以证明性能优化的重要性。
性能优化的原则遵循二八定律。在进行优化时,想要尽善尽美往往是非常难的。我们应优先处理亟需优化的关键部分,如此便可获得大部分效益了。剩下的部分很可能投入较多,但收益并不大。
Go 语言本身是面向服务端软件设计的,在 Go SDK 中自带了 pprof 性能分析工具。具体说来,有两个相关的包:runtime/pprof 和 net/http/pprof。后者是对前者的封装,用于提供网络侧访问。
本讲内容将从 CPU 和内存分析两方面介绍 pprof 的使用。
# 生成运行报告
假设现在有如下代码,它实现了对 10000 个随机整数的排序:
go
复制代码func randomIntCreator() []int {
var arr []int
for i := 0; i < 10000; i++ {
arr = append(arr, rand.Intn(100000000))
}
return arr
}
func sort() {
arr := randomIntCreator()
var n = len(arr)
for i := 0; i <= n-1; i++ {
for j := i; j <= n-1; j++ {
if arr[i] > arr[j] {
t := arr[i]
arr[i] = arr[j]
arr[j] = t
}
}
}
}
若要对其进行压力测试,观察其运行 100 次的稳定性和耗时情况,仅需如下调用即可:
go
复制代码func main() {
f, _ := os.OpenFile("cpu.pprof", os.O_CREATE|os.O_RDWR, 0644)
pprof.StartCPUProfile(f)
defer f.Close()
defer pprof.StopCPUProfile()
for i := 0; i < 100; i++ {
sort()
}
}
💡 提示:压力测试是非常实用的测试方法,它能模拟高并发、大流量等情况,从而衡量程序运行的稳定性和正确性。某些问题可能尽在运行压力较大时才会显现,某些性能问题在程序反复执行后更加明显。
如上代码所示,main() 函数中创建了 cpu.pprof 文件并将返回的 *File 值赋给变量 f,紧接着调用 pprof.StartCPUProfile() 函数传入 f 变量,开始执行 CPU 运行分析。
在之后使用断言关闭文件,调用 pprof.StopCPUProfile() 停止 CPU 运行分析。最后是循环 100 次调用 sort() 函数,进行测试。程序运行时,首先会尝试打开 cpu.pprof 文件,当该文件不存在时则会创建它。紧接着便会开始 CPU 分析。随后开始 100 次执行 sort() 函数。执行后停止 CPU 分析,关闭文件。
运行上述代码,稍等片刻,控制台便会输出:
Process finished with the exit code 0
表示程序正常退出,结束运行。此时来到项目根目录下,可以看到 cpu.pprof 文件。文件中记录着从开始 CPU 分析到停止 CPU 分析之间的函数耗时情况。
# 交互式终端视图
打开终端,定位到项目根目录下,执行:
shell
复制代码go tool pprof cpu.pprof
便可启动交互式终端视图,获取文件中保存的各种信息了,运行结果示例如下:
Type: cpu
Time: May 18, 2022 at 10:11am (CST)
Duration: 12.73s, Total samples = 8.28s (65.03%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
可以看到,上述文字说明:测试类型是 CPU,测试时间是 2022 年 5 月 18 日 早上 10:11,程序运行总时长是 12.73 秒,测试采样时间是 8.28 秒,占总时长的 65.03%。倒数第 2 行是命令说明:输入 help 显示帮助信息,列出支持的命令;输入 o 进行选项设置。最后一行是等待用户输入行。
较为常用的是 top 命令,它能列出函数的执行时长,当我们成功进入交互式终端,并执行 top 命令后,可以看到如下结果输出:
Showing nodes accounting for 8.14s, 98.31% of 8.28s total
Dropped 29 nodes (cum <= 0.04s)
Showing top 10 nodes out of 18
flat flat% sum% cum cum%
7.97s 96.26% 96.26% 8.06s 97.34% main.sort
0.09s 1.09% 97.34% 0.09s 1.09% runtime.asyncPreempt
0.05s 0.6% 97.95% 0.05s 0.6% runtime.stdcall1
0.02s 0.24% 98.19% 0.10s 1.21% runtime.findrunnable
0.01s 0.12% 98.31% 0.05s 0.6% runtime.wakep
0 0% 98.31% 8.06s 97.34% main.main
0 0% 98.31% 0.05s 0.6% runtime.bgscavenge.func2
0 0% 98.31% 0.05s 0.6% runtime.checkTimers
0 0% 98.31% 8.06s 97.34% runtime.main
0 0% 98.31% 0.15s 1.81% runtime.mcall
我们从上往下对上述输出进行解读:
- 第 1 行的意思是以下信息显示了总计耗时 8.14 秒的函数执行情况,占总耗时的 98.31%。
- 第 2 行的意思是被省略的执行函数总计个数:29,它们的总耗时小于等于 0.04 秒。
- 第 3 行的意思是以下表格列出了被执行的 18 个函数,以耗时从长到短排序,前 10 个函数。
显然,sort() 函数耗时 7.97秒,自身占比 96.26%。其次还可以找到 main() 函数,其中并没有什么实际的逻辑,但 sort() 函数在其中,所以总时长是 8.06 秒,累计占比 98.31%,但自身时长和占比都是 0。
表格中的 5 列数据的含义如下:
列名 | 含义 |
---|---|
flat | 函数的执行耗时 |
flat% | 函数的执行耗时占 CPU 总时间的比例 |
sum% | 前面每一行的函数的执行耗时占比的总和 |
cum | 该函数加上该函数调用的函数的总耗时 |
cum% | 该函数加上该函数调用的函数的总耗时占 CPU 总时间的比例 |
可见,通过在代码的特定位置启动和停止 CPU 分析,可以很好地测量被其包含的代码运行状况。非常适用于一次运行的情况,即只跑一次程序,手动采集性能数据。
# Web 可视化视图
Web 可视化视图提供了查看性能分析结果的另一种方式,它更加直观。特别是对于函数调用的层次结构而言,看上去会更加方便。
对于本例,在进入终端交互视图后,执行 web 命令,便会打开系统默认的浏览器,显示如下内容:
为了方便查看,网页支持使用鼠标左键拖拽视图显示的内容,也支持通过滚轮进行视图缩放。
💡 提示:正常情况下,上图所示的分析结果与终端输出的结果相同。在编写本讲时,我运行了两次,所以结果略有不同。
# 实时性能分析
正如前文中所述:在代码特定的位置进行 CPU 检测可以达到只跑一次程序,便能采集性能数据的情况。但现实中往往还需要实时观测内存的占用情况,此时,net/http/pprof 包就派上用场了。
还是对随机数排序的案例,将 main() 函数稍加修改,便能通过 Web 页观测内存使用情况了,具体如下:
go
复制代码import _ "net/http/pprof"
func main() {
go func() {
err := http.ListenAndServe("127.0.0.1:9090", nil)
if err != nil {
panic(err)
}
}()
for i := 0; i >= 0; i++ {
fmt.Printf("运行第%d次\n", i+1)
sort()
time.Sleep(1 * time.Second)
}
}
可以看到,程序一上来启动了一个协程,目的是开启本地 http 服务器,响应 9090 端口。然后便是 for 循环了,请大家特别留意 for 循环的终止条件,此处改为 i>=0。显然这个循环将一直持续下去,直到程序被强行停止。循环体的内容也做了一些调整,即每隔 1 秒执行一次随机数的生成和排序,并输出运行次数。
❗️ 注意:通过 Web 观察 pprof 实时内存测试结果,必须导入 net/http/pprof 包。
运行上述代码,可以看到控制台有稳定的文字输出。然后适用浏览器打开:http://localhost:9090/debug/pprof/ (opens new window) ,可以看到如下页面:
我们可以点击上方的链接进入某个具体的项目查看详情。以下是我在程序刚开始运行和循环体执行了 10 次后的内存占用概览信息:
程序刚开始运行:
# runtime.MemStats
# Alloc = 1123208
# TotalAlloc = 1123208
# Sys = 8407496
# Lookups = 0
# Mallocs = 2138
# Frees = 143
# HeapAlloc = 1123208
# HeapSys = 4030464
# HeapIdle = 1818624
# HeapInuse = 2211840
# HeapReleased = 1818624
# HeapObjects = 1995
# Stack = 163840 / 163840
# MSpan = 37400 / 49152
# MCache = 4672 / 16384
# BuckHashSys = 1447574
# GCSys = 1931744
# OtherSys = 768338
# NextGC = 4473924
# LastGC = 0
循环体执行约 10 次后:
# runtime.MemStats
# Alloc = 1130216
# TotalAlloc = 4646560
# Sys = 13588888
# Lookups = 0
# Mallocs = 2606
# Frees = 1442
# HeapAlloc = 1130216
# HeapSys = 8192000
# HeapIdle = 6242304
# HeapInuse = 1949696
# HeapReleased = 3850240
# HeapObjects = 1164
# Stack = 196608/ 196608
# MSpan = 46104/ 49152
# MCache = 4672/ 16384
# BuckHashSys = 1448454
# GCSys = 2638432
# OtherSys = 1047858
# NextGC = 4194304
# LastGC = 1652842675951237400
需要特别说明的是 trace,当我们点击 trace 后,会下载到一个文件。这个文件的名称默认为 trace。通过在终端执行:
shell
复制代码go tool trace trace
这句命令中,第 2 个trace 指文件名。执行后,可以观测到完整的跟踪等信息,这些信息同样通过浏览器查看:
如上图所示,在完整的 trace 文件中包含了众多实用的状态数据。比如,当我们点击 View Trace 时,可以看到具体的函数调用时长等信息,如下图所示:
# 小结
🎉 恭喜,您完成了全部课程的学习!
📌 以下是本次课程的重点内容总结:
- Go 程序性能分析工具的使用
正如本讲开头所述:进行性能优化的收益不仅是节省下来的运行时间,更是硬件成本的投入和运行稳定性的保证。在进行性能优化时,应遵循二八原则,首要任务是解决最影响性能的问题点。
Go SDK 中包含了进行性能分析的工具:pprof。本讲主要介绍了使用它来进行 CPU 和内存的性能分析。此外,pprof 还能获取锁竞争状态、线程/协程创建情况等数据,这里就不再一一赘述了。