原文链接:https://www.hezebin.com/article/66a8c83c4379b36dec11a197
什么时候做性能分析?
性能分析是一种评估软件系统性能的方法,它涉及测量和评估程序或系统的运行效率,包括但不限于处理速度、资源使用效率、响应时间等。
当你的系统存在性能瓶颈,想要优化代码、提高系统的整体性能和响应速度的时候,就需要做性能分析了。
所以其实只有在高负载时,性能分析才有意义!
性能分析的阶段
性能分析可以在软件系统开发和维护的多个阶段进行,如在【需求分析和技术设计阶段】
就可以通过良好的架构设计、数据结构和算法选择来满足性能需求。对于 Golang 程序开发人员,针对语言生态和特性,本文主要通过聊聊【开发阶段】
运行数据采集、【测试阶段】
性能指标测试验证和【生产环境监控阶段】
的问题排查来介绍如何在 Go 程序中做性能分析。
数据采集
Golang 是一个对性能特别看重的语言,官方自带了性能分析相关的工具和库 pprof
(Performance Profiling),对于我们常见的微服务HTTP应用,只需要简单的 2 行代码即可实现对数据的实时采集:
package main
import "net/http"
import _ "net/http/pprof"
func main() {
// 启动一个HTTP服务器,用于访问pprof相关数据
http.ListenAndServe(":6060", nil)
// 你的程序逻辑
}
可以看到只需要引入net/http/pprof
包,然后启动一个使用默认 handler 的DefaultServeMux
的 HTTP 服务。查看源码可以看到 net/http/pprof
包的初始化函数init
向默认 DefaultServeMux
中注册了一些以/debug/pprof
开头的路由:
浏览器访问:http://locahost:6060/debug/pprof
打开的页面下除了有上述init
中显式注册的路由外,还有一些其他子页面路由,这些路由通过Index
动态的去处理:
然后在不同的路由下,去找到不同的 runtime/pprof.Profile
做对应的数据采集:
上述各个采集项的描述如下:
类型 | 描述 |
---|---|
allocs | 内存分配情况的采样信息 |
blocks | 阻塞操作情况的采样信息 |
cmdline | 显示程序启动命令及参数 |
goroutine | 当前所有协程的堆栈信息 |
heap | 堆上内存使用情况的采样信息 |
mutex | 锁争用情况的采样信息 |
profile | CPU 占用情况的采样信息 |
threadcreate | 系统线程创建情况的采样信息 |
trace | 程序运行跟踪信息 |
进一步查看如对 CPU 指标数据的采集源码:
其本质还是通过调用 runtime/pprof
包下的方法,采集一段时间内的数据。所以对于非 Web 类型的应用程序,如脚本程序运行一段时间就结束了,要对其实现性能分线前的数据采集,示例如下:
import (
"log"
"os"
"path/filepath"
"runtime/pprof"
)
// 进行CPU监控
func CreateProfileFile() {
dir, err := os.Getwd()
if err != nil {
log.Fatalln("get current directory failed.", err)
}
fileName := filepath.Join(dir, "pprof", "profile_file", "profile_file")
f, _ := os.Create(fileName)
// start to record CPU profile and write to file `f`
_ = pprof.StartCPUProfile(f)
// stop to record CPU profile
defer pprof.StopCPUProfile()
// TODO do something
}
如果使用自定义的 Mux
,则需要像上述init
一样手动注册一些路由规则。如使用gin
框架,可以直接使用github.com/gin-contrib/pprof
包的pprof.Register
函数,其本质也是手动注册:
// Register the standard HandlerFuncs from the net/http/pprof package with
// the provided gin.Engine. prefixOptions is a optional. If not prefixOptions,
// the default path prefix is used, otherwise first prefixOptions will be path prefix.
func Register(r *gin.Engine, prefixOptions ...string) {
RouteRegister(&(r.RouterGroup), prefixOptions...)
}
// RouteRegister the standard HandlerFuncs from the net/http/pprof package with
// the provided gin.GrouterGroup. prefixOptions is a optional. If not prefixOptions,
// the default path prefix is used, otherwise first prefixOptions will be path prefix.
func RouteRegister(rg *gin.RouterGroup, prefixOptions ...string) {
prefix := getPrefix(prefixOptions...)
prefixRouter := rg.Group(prefix)
{
prefixRouter.GET("/", gin.WrapF(pprof.Index))
prefixRouter.GET("/cmdline", gin.WrapF(pprof.Cmdline))
prefixRouter.GET("/profile", gin.WrapF(pprof.Profile))
prefixRouter.POST("/symbol", gin.WrapF(pprof.Symbol))
prefixRouter.GET("/symbol", gin.WrapF(pprof.Symbol))
prefixRouter.GET("/trace", gin.WrapF(pprof.Trace))
prefixRouter.GET("/allocs", gin.WrapH(pprof.Handler("allocs")))
prefixRouter.GET("/block", gin.WrapH(pprof.Handler("block")))
prefixRouter.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
prefixRouter.GET("/heap", gin.WrapH(pprof.Handler("heap")))
prefixRouter.GET("/mutex", gin.WrapH(pprof.Handler("mutex")))
prefixRouter.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
}
}
性能分析
!!! example 示例项目可参考
https://github.com/ihezebin/go-template-ddd
!!!
上述采集到的数据,不论是 Web 程序浏览器访问还是非 Web 程序的数据文件其内容可读性都非常差:
这样的数据格式和数据量很难直观的分析出程序的性能表现情况。对此,Golang 的 pprof
工具自带了 go tool pprof
命令对采集的数据进行更生动和友好的展示!
终端分析
下述命令执行后将进入一个交互式终端,完整命令:
go tool pprof [binary] file
- binary:正在执行的二进制可执行程序,可选。
- file:pprof监控生成的文件。可以是具体的文件如profile.pprof,也可以是web站点的地址,如http://localhost:6060/debug/pprof/profile。
在终端中输入help
可查看可用的命令,如top
命令将排序列出资源使用较高的调用:
又如通过list 代码命令
(支持正则),list emitLocation
可以查看函数和代码情况:
比较好用的一个命令是web
,该命令可以在Web Browser上图形化显示当前的资源监控内容,不过需要事先安装 Graphviz(tips:依赖的东西有点多,下载安装时间可能有点久)。
web
命令的实际行为是生成一个 .svg
文件,并调用系统里设置的默认打开 .svg
的程序打开它。如果系统里打开 .svg
的默认程序并不是浏览器(比如代码编辑器),需要设置一下默认使用浏览器打开 .svg
文件。然后浏览器自动打开:
该图更好的展示了调用层级关系和资源占用情况,方便我们更高效的定位到高资源消耗或占用的位置。
如果需要其他命令,可以在pprof交互式终端里通过help
查看其他命令的使用方法。例如:
- svg:生成svg图。
- pdf:生成pdf文件,显示svg图。
- png:生成png图片,显示svg图。
… 更多其他的命令和选项,请自行学习验证。
除了在交互终端中导出 svg 外,还可以不进入终端导出:
go tool pprof -svg allocs > allocs.svg
可视化分析
最新版的 Golang pprof
(v1.10+)已经支持动态的 Web 浏览方式查看所有类型的资源监控图:
go tool pprof -http=":6060" http://localhost:8080/debug/pprof/allocs
该命令会启动一个Web服务器:http://localhost:6061
。一般会自动弹出打开浏览器页面并显示分析结果,默认显示的是 Graph
。但是可以从第一行的菜单中切换 View
,选择 Flame Graph
即可显示火焰图:
性能测试
go test
是 Go 语言官方的测试工具,用于运行包中的测试函数。
单元测试
Go 语言的测试函数遵循一定的命名规则,通常是以 Test
开头,后面跟着一个大写字母,然后是测试用例的描述。例如 `TestExample。测试函数不需要参数,也不需要返回值。
go test 的基本用法如下:
go test [包名] [标志]
例如,如果你有一个名为 example 的包,你可以使用以下命令来运行该包中的所有测试:
go test example
如下是一个单元测试示例,假设你有一个简单的 math
包,里面有一个 Add
函数,你想要为这个函数写一个单元测试:
// math.go
package math
func Add(a, b int) int {
return a + b
}
单元测试文件通常命名为 *_test.go
,例如 math_test.go
:
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
if Add(1, 2) != 3 {
t.Errorf("Add(1, 2) = %d; expected 3", Add(1, 2))
}
}
运行单元测试:
go test
基准测试
-bench
是 go test
的一个选项参数,用于基准测试。基准测试是一种性能测试,用于测量代码段的执行时间。使用 -bench
时,你可以指定要运行的基准测试函数的正则表达式模式。
-bench
的基本用法如下:
go test -bench [模式] [包名或文件]
例如,如果你想要运行所有以 Benchmark
开头的基准测试函数,你可以使用以下命令:
go test -bench .
这里的 .
表示当前目录下的所有 Go 文件。
如下是一个基准测试示例,如果你想要测量 Add 函数的性能,可以写一个基准测试:
// math_test.go
package math
import "testing"
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
基准测试函数接收一个 *testing.B
类型的参数,b.N
是一个非常大的数字,表示循环的次数。基准测试会运行多次循环,以减少测量误差。
运行基准测试:
go test -bench BenchmarkAdd
请注意,基准测试的输出会告诉你每次操作的纳秒数,以及总共运行了多少次。
test 集成 pprof
go test
命令有两个参数和 pprof
相关,它们分别指定生成的 CPU 和 Memory Profiling 保存的文件:
cpuprofile
:cpu profiling 数据要保存的文件地址memprofile
:memory profiling 数据要报文的文件地址
比如下面执行测试的同时,也会执行 CPU Profiling,并把结果保存在 cpu.prof
文件中:
go test -bench . -cpuprofile=cpu.prof
执行结束之后,就会生成 main.test
和 cpu.prof
文件。要想使用 go tool pprof
,需要指定的二进制文件就是 main.test
。
通常情况下,Profiling 一般和性能测试一起使用,这个原因在前文也提到过,只有应用在负载高的情况下 Profiling 才有意义。
参考
- https://www.jianshu.com/p/6175798c03b4
- https://eddycjy.com/posts/go/tools/2018-09-15-go-tool-pprof/#%E6%80%BB%E7%BB%93
- https://blog.csdn.net/cbmljs/article/details/86642669