1 介绍
在Golang中,可以通过pprof工具对应于程序的运行时进行性能分析,包括CPU、内存、Goroutine等实时信息。
Golang内置了获取程序运行数据的工具,包括以下两个标准库:
- runtime/pprof: 采集工具型应用运行数据进行分析
- net/http/pprof: 采集服务型应用运行时数据进行分析
pprof开启后,每隔一段时间(10ms)就会收集当前的堆栈信息,获取各个函数占用的CPU以及内存资源,然后通过对这些采样数据进行分析,形成一个性能分析报告。性能优化主要有以下几个方面:
- CPU Profile:报告程序的CPU使用情况,按照100 Hz频率去采集应用程序在CPU和寄存器上面的数据。
- Memory Profile(Heap Profile):报告程序的内存使用情况,其实就是程序运行过程中堆内存的分配情况,默认每分配512K字节时取样一次。
- Block Profiling: 报告goroutines不在运行状态的情况,保存用户程序中的Goroutine阻塞事件的记录,可以用来分析和查找死锁等性能瓶颈。
- Goroutine Profiling: 报告goroutines的使用情况,有哪些roroutines,它们的调用关系是怎样的。
默认情况下是不追踪block和mutex的信息的,如果想要看这两个信息,需要在代码中加上两行:
代码语言:go复制runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪,block
runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪,mutex
引入pprof
代码语言:go复制import _ "net/http/pprof"
go func() {
http.ListenAndServe("0.0.0.0:6060", nil)
}()
在浏览器中输入地址:http://127.0.0.1:6060/debug/pprof/,即可看到监控的数据。
profiles解释如下:
- allocs:查看过去所有内存分配的样本。
- block:查看导致阻塞同步的堆栈跟踪。
- cmdline: 当前程序的命令行的完整调用路径。
- goroutine:查看当前所有运行的 goroutines 堆栈跟踪。
- heap:查看活动对象的内存分配情况。
- mutex:查看导致互斥锁的竞争持有者的堆栈跟踪。
- profile: 默认进行 30s 的 CPU Profiling,得到一个分析用的 profile 文件。
- threadcreate:查看创建新 OS 线程的堆栈跟踪。
- trace:能够辅助我们跟踪程序的执行情况。
2 分析
线上分析用的更多的是go tool pprof,因为本地浏览器不能直接访问线上服务地址,而是通过线上服务生成分析文件,下载文件到本地进行分析,需要在本地安装graphviz工具(https://graphviz.org/download/)。
线上使用go tool pprof采集的时候可以通过加入?seconds=30参数来指定采集时间。
2.1 查看CPU使用情况
查看CPU使用率排行命令如下:
代码语言:shell复制go tool pprof http://9.9.9.9:8080/debug/pprof/profile
默认30秒后会进入交互模式,常见命令如下
- help可以查看使用说明
- top 可以查看前10个的CPU使用情况
- tree 以树状显示
- png 以图片格式输出
- svg 生成浏览器可以识别的svg文件
可使用top命令查看CPU使用率排行,top N显示前N个函数,默认显示10个。
代码语言:shell复制go tool pprof http://9.9.9.9:8080/debug/pprof/profile
Fetching profile over HTTP from http://9.9.9.9:8080/debug/pprof/profile
Saved profile in /root/pprof/pprof.featureservice.samples.cpu.001.pb.gz
File: featureservice
Type: cpu
Time: Jul 1, 2022 at 10:29am (CST)
Duration: 30.19s, Total samples = 1.17mins (231.79%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 30
Showing nodes accounting for 0.73mins, 62.18% of 1.17mins total
Dropped 661 nodes (cum <= 0.01mins)
Showing top 30 nodes out of 190
flat flat% sum% cum cum%
0.06mins 5.25% 5.25% 0.11mins 9.66% github.com/valyala/bytebufferpool.(*Pool).Put
0.05mins 4.54% 9.79% 0.06mins 4.99% github.com/valyala/bytebufferpool.(*Pool).Get
0.05mins 4.12% 13.91% 0.05mins 4.12% github.com/valyala/bytebufferpool.index (inline)
0.04mins 3.77% 17.68% 0.08mins 7.02% runtime.mapaccess2_faststr
0.04mins 3.49% 21.17% 0.06mins 5.26% runtime.mapaccess2_fast64
0.04mins 3.13% 24.30% 0.13mins 10.88% runtime.mallocgc
0.04mins 3.12% 27.41% 0.04mins 3.24% syscall.Syscall
0.04mins 3.00% 30.41% 0.17mins 14.26% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.processModelTensorFeature
0.03mins 2.47% 32.89% 0.03mins 2.47% memeqbody
0.03mins 2.22% 35.10% 0.03mins 2.22% runtime.memhash64
0.02mins 1.87% 36.97% 0.03mins 2.72% runtime.mapaccess1_fast64
0.02mins 1.84% 38.82% 0.03mins 2.32% runtime.heapBitsSetType
0.02mins 1.67% 40.49% 0.62mins 53.31% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.processItemRelatedTensorFeatureOfCreativeItem
0.02mins 1.66% 42.15% 0.02mins 1.70% strconv.readFloat
0.02mins 1.59% 43.73% 0.06mins 5.13% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.ParseFloat32ByInterface
0.02mins 1.51% 45.25% 0.04mins 3.07% runtime.mapaccess1_faststr
0.02mins 1.50% 46.75% 0.02mins 1.50% runtime.futex
0.02mins 1.50% 48.25% 0.05mins 4.69% runtime.scanobject
0.02mins 1.47% 49.72% 0.02mins 1.47% runtime.memmove
0.02mins 1.43% 51.15% 0.02mins 1.43% runtime.nextFreeFast (inline)
0.02mins 1.34% 52.49% 0.02mins 1.34% aeshashbody
0.02mins 1.33% 53.82% 0.02mins 1.33% runtime.memclrNoHeapPointers
0.01mins 1.14% 54.97% 0.02mins 1.31% runtime.findObject
0.01mins 1.10% 56.07% 0.02mins 1.37% runtime.lock2
0.01mins 1.09% 57.15% 0.11mins 9.70% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.getFeatureStorageKey
0.01mins 1.09% 58.24% 0.01mins 1.09% strconv.special
0.01mins 1.03% 59.27% 0.24mins 20.54% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.getCrossFeatureFieldKey
0.01mins 0.99% 60.25% 0.03mins 2.43% strconv.formatBits
0.01mins 0.97% 61.23% 0.01mins 0.99% runtime.unlock2
0.01mins 0.96% 62.18% 0.01mins 0.96% strconv.ParseUint
输出内容最后一列为函数名称,其他各项内容意义如下:
- flat:当前函数占用CPU的耗时
- flat%:当前函数占用CPU的耗时百分比
- sum%:函数占用CPU的累积耗时百分比
- cum:当前函数 调用当前函数的占用CPU总耗时
- cum%:当前函数 调用当前函数的占用CPU总耗时百分比 list命令可以查看函数的cpu使用情况,形式为 list 函数名,这种需要调用源码,在线上打包后的一般看不到,本地运行可查看具体耗时情况。
(pprof) list List
Total: 140ms
ROUTINE ======================== controller/btracks.List in /Users/gyw/go/btracks.go
0 10ms (flat, cum) 7.14% of Total
. . 17: if !xc.GetReqObject(&req) {
. . 18: Logger("unmarshal error")
. . 19: return
. . 20: }
. . 21: id := xc.Request.Header.Get(def.XMidKeyName)
. 10ms 22: Logger(xc, "list get mid", "mid", mid)
. . 23:
. . 24: resp, err := btrackService.List(xc, &req, mid)
. . 25: if err != nil {
. . 26: Logger("fail list", "err", err, "req", req, "mid", mid)
. . 27: xc.Reply(lib.CodeNotExist)
(pprof)
traces命令可以调出函数的调用栈,形式为traces 函数名。
若没有安装Golang及相关工具,可以直接wget获取profile信息到文件中进行本地分析。
代码语言:shell复制wget -O pprof.featureservice.profile '9.9.9.9:8080/debug/pprof/profile?seconds=30'
在上述go tool pprof命令输出内容中可以看到已保存采集的信息到本地文件,/root/pprof/pprof.featureservice.samples.cpu.001.pb.gz,下载该文件到本地,运行如下命令,即可在本地启动一个web服务(监听8080端口),在浏览器可以查看更多信息。
代码语言:txt复制go tool pprof -http=:8080 pprof.featureservice.samples.cpu.001.pb.gz
浏览器输入http://127.0.0.1:8080/ui/
菜单解释
- Top:类和命令行中的top命令类似
- Graph:函数调用的连线图
- Flame Graph:火焰图
- Peek:文本形式的top函数调用栈
- Source:显示top函数源码文件位置
图中的部分信息解释
- 节点的颜色越红,其cum和cum%越大。其颜色越灰白,则cum和cum%越小。
- 节点越大,其flat和flat%越大;其越小,则flat和flat%越小。
- 线条代表了函数的调用链,线条越粗,代表指向的函数消耗了越多的资源。反之亦然。
- 线条的样式代表了调用关系。实线代表直接调用;虚线代表中间少了几个节点;带有inline字段表示该函数被内联进了调用方。
Source可以列出函数调⽤的源代码,为了让该视图正确的显⽰,我们必须使⽤go tool pprof命令,⽽不能直接使⽤pprof命令,命令⾏的pprof版本有可能⽆法使源码正确的呈现出来。如果要使⽤Source视图,还需要注意将编译好的⼆进制⽂件作为参数传⼊,如:
代码语言:txt复制go tool pprof -http=:8080 featureservice pprof.featureservice.samples.cpu.001.pb.gz
Disassemable和Source类似,也是显⽰源代码的,但是Disassemable显⽰的是汇编语⾔。Disassemable的正确展⽰也和Source⼀样的要求。
2.2 查看内存使用情况
查看内存使用排行命令如下:
代码语言:shell复制go tool pprof http://9.9.9.9:8080/debug/pprof/heap
会很快进入交互模式,可使用top命令查看内存使用排行,top N显示前N个函数,默认显示10个。
代码语言:shell复制go tool pprof http://9.9.9.9:8080/debug/pprof/heap
Fetching profile over HTTP from http://9.9.9.9:8080/debug/pprof/heap
Saved profile in /root/pprof/pprof.featureservice.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
File: featureservice
Type: inuse_space
Time: Jul 1, 2022 at 10:30am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 30
Showing nodes accounting for 235.14MB, 93.76% of 250.79MB total
Dropped 124 nodes (cum <= 1.25MB)
Showing top 30 nodes out of 203
flat flat% sum% cum cum%
30MB 11.96% 11.96% 30MB 11.96% reflect.New
20.48MB 8.17% 20.13% 20.48MB 8.17% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.GetTensorProtoFloatSlice.func1
18.09MB 7.21% 27.34% 18.09MB 7.21% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.GetTensorProtoIntSlice.func1
17.59MB 7.02% 34.36% 17.59MB 7.02% git.code.oa.com/polaris/polaris-go/pkg/metric.NewResMetricArray (inline)
17.52MB 6.98% 41.34% 17.52MB 6.98% runtime.allocm
14MB 5.58% 46.93% 14MB 5.58% git.code.oa.com/polaris/polaris-go/pkg/metric.NewSliceWindow (inline)
13.79MB 5.50% 52.42% 28.03MB 11.18% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service/config.updateLocalCacheLoadedFeatureCache
10MB 3.99% 56.41% 17.50MB 6.98% git.code.oa.com/polaris/polaris-go/pkg/metric.(*SliceWindow).initBucket
8.63MB 3.44% 59.86% 8.63MB 3.44% reflect.mapassign
8.50MB 3.39% 63.24% 8.50MB 3.39% github.com/go-redis/redis/v8/internal/proto.(*Reader).readStringReply
7.96MB 3.17% 66.42% 7.96MB 3.17% git.code.oa.com/polaris/polaris-go/plugin/loadbalancer/maglev.NewTable
6.75MB 2.69% 69.11% 6.75MB 2.69% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service/config.updateLocalCacheBatchHmgetLoadedFeatureCache
6.04MB 2.41% 71.52% 6.04MB 2.41% bufio.NewReaderSize
5.52MB 2.20% 73.72% 5.52MB 2.20% bufio.NewWriterSize
5.50MB 2.19% 75.91% 5.50MB 2.19% git.code.oa.com/polaris/polaris-go/pkg/model/local.(*DefaultInstanceLocalValue).SetSliceWindows (inline)
5.50MB 2.19% 78.11% 5.50MB 2.19% git.code.oa.com/polaris/polaris-go/pkg/model/pb.NewInstanceInProto
5MB 2.00% 80.10% 11.51MB 4.59% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.processItemRelatedTensorFeatureOfCreativeItem
4.54MB 1.81% 81.91% 44.12MB 17.59% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.processModelTensorFeature
4.50MB 1.80% 83.71% 6MB 2.39% sync.(*Map).Store
4MB 1.60% 85.31% 4MB 1.60% strings.genSplit
4MB 1.60% 86.90% 4MB 1.60% encoding/json.(*decodeState).literalStore
3MB 1.20% 88.10% 3MB 1.20% git.code.oa.com/polaris/polaris-go/pkg/model/local.NewInstanceLocalValue
2.50MB 1% 89.09% 2.50MB 1% git.code.oa.com/polaris/polaris-go/plugin/statreporter/serviceinfo.newStatusList
2.16MB 0.86% 89.95% 2.66MB 1.06% google.golang.org/protobuf/proto.MarshalOptions.marshal
1.95MB 0.78% 90.73% 2.45MB 0.98% git.woa.com/judezhang/yky-feature-service/internal/featureservice/service.init
1.59MB 0.63% 91.37% 1.59MB 0.63% git.code.oa.com/polaris/polaris-go/plugin/serverconnector/grpc.NewAsyncRateLimitConnector
1.50MB 0.6% 91.97% 1.50MB 0.6% runtime.malg
1.50MB 0.6% 92.56% 1.50MB 0.6% sync.(*Map).LoadOrStore
1.50MB 0.6% 93.16% 11MB 4.39% git.code.oa.com/polaris/polaris-go/plugin/circuitbreaker/errorcount.(*CircuitBreaker).generateSliceWindow
1.50MB 0.6% 93.76% 1.50MB 0.6% reflect.cvtBytesString
在上述go tool pprof命令输出内容中可以看到已保存采集的信息到本地文件,/root/pprof/pprof.featureservice.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz,下载该文件到本地,运行如下命令,即可在本地启动一个web服务(监听8080端口),在浏览器可以查看更多信息。
代码语言:shell复制go tool pprof -http=:8081 pprof.featureservice.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
浏览器输入http://localhost:8081/ui/
2.3 查看trace使用情况
查看trace命令如下:
代码语言:shell复制curl http://9.9.9.9:8080/debug/pprof/trace?seconds=10 > trace.out
下载该文件到本地,运行如下命令,即可在本地启动一个web服务(监听8080端口),在浏览器可以查看更多信息。
代码语言:shell复制go tool trace -http=127.0.0.1:8082 trace.out
打开浏览器输入http://127.0.0.1:8082/,即可看到如下信息
链接解释
- View trace:以图形页面的形式渲染和展示tracer的数据,这是最常用的功能。
- Goroutine analysis:以表格形式记录执行同一个函数的多个goroutine的各项trace数据
- Network blocking profile:用pprof profile形式的调用关系图展示网络I/O阻塞的情况
- Synchronization blocking profile:用pprof profile形式的调用关系图展示同步阻塞耗时情况
- Syscall blocking profile:用pprof profile形式的调用关系图展示系统调用阻塞耗时情况
- Scheduler latency profile:用pprof profile形式的调用关系图展示调度器延迟情况
- User-defined tasks和User-defined regions:用户自定义trace的task和region
- Minimum mutator utilization:分析GC对应用延迟和吞吐影响情况的曲线图
时间线
为View trace提供了时间参照系,View trace的时间线始于Tracer开启时,各个区域记录的事件的时间都是基于时间线的起始时间的相对时间。
时间线的时间精度最高为纳秒,但View trace视图支持自由缩放时间线的时间标尺,我们可以在秒、毫秒的“宏观尺度”查看全局,我们亦可以将时间标尺缩放到微秒、纳秒的“微观尺度”来查看某一个极短暂事件的细节。
采样状态区(Stats)
这个区内展示了三个指标:Goroutines、Heap和Threads,某个时间点上的这三个指标的数据是这个时间点上的状态快照采样:
- Goroutines:某一时间点上应用中启动的goroutine的数量,当我们点击某个时间点上的goroutines采样状态区域时(我们可以用快捷键m来准确标记出那个时间点),事件详情区会显示当前的goroutines指标采样状态。
- Heap指标则显示了某个时间点上Go应用heap分配情况(包括已经分配的Allocated和下一次GC的目标值NextGC)。
- Threads指标显示了某个时间点上Go应用启动的线程数量情况,事件详情区将显示处于InSyscall(整阻塞在系统调用上)和Running两个状态的线程数量情况。
P区(PROCS)
我们能看到Go应用中每个P(Goroutine调度概念中的P)上发生的所有事件,包括:EventProcStart、EventProcStop、EventGoStart、EventGoStop、EventGoPreempt、Goroutine辅助GC的各种事件以及Goroutine的GC阻塞(STW)、系统调用阻塞、网络阻塞以及同步原语阻塞(mutex)等事件。除了每个P上发生的事件,我们还可以看到以单独行显示的GC过程中的所有事件。
另外我们看到每个Proc对应的条带都有两行,上面一行表示的是运行在该P上的Goroutine的主事件,而第二行则是一些其他事件,比如系统调用、运行时事件等,或是goroutine代表运行时完成的一些任务,比如代表GC进行并行标记。
在G区,每个G都会对应一个单独的条带(和P区一样,每个条带都有两行),通过这一条带我们可以按时间线看到这个G的全部执行情况。通常我们仅需在goroutine analysis的表格页面找出执行最快和最慢的两个goroutine,在Go视角视图中沿着时间线对它们进行对比,以试图找出执行慢的goroutine究竟出了什么问题。
2.4 Go test 使用pprof
Golang在运行测试用例或压测时也可以通过添加参加输出测试过程中的CPU、内存和trace情况,分析过程同上,参数如下:
-bench=. // 进行性能测试,“.”是正则匹配,匹配了所有的测试函数
-benchmem // 打印出申请内存的次数。一般用于简单的性能测试,不会导出数据文件
-blockprofile block.out // 将协程的阻塞数据写入特定的文件(block.out),如果-c,则写成二进制文件
-cpuprofile cpu.out // 将协程的CPU使用数据写入特定的文件(cpu.out),如果-c,则写成二进制文件
-memprofile mem.out // 将协程的内存申请数据写入特定的文件(mem.out),如果-c,则写成二进制文件
-mutexprofile mutex.out // 将协程的互斥数据写入特定的文件(mutex.out),如果-c,则写成二进制文件
-trace trace.out // 将执行调用链写入特定文件(trace.out)
3 参考
3.1 top输出列解释
通过一个简单示例函数来解释flat、cum、sum等参数含义。
代码语言:go复制func foo(){
a() // step1
largeArray := [math.MaxInt64]int64{} // step2
for i := 0; i < math.MaxInt64; i { // step3
c() // step4
}
}
flat flat%
flat只会记录step2和step3的时间;flat%即是flat/总运行时间。内存等参数同理。所有的flat相加即是总采样时间,所有的flat%相加应该等于100%。
flat一般是我们最关注的。其代表一个函数可能非常耗时,或者调用了非常多次,或者两者兼而有之,从而导致这个函数消耗了最多的时间。
如果是我们自己编写的代码,则很可能有一些无脑for循环、复杂的计算、字符串操作、频繁申请内存等等。
如果是第三方库的代码,则很可能我们过于频繁地调用了这些第三方库,或者以不正确的方式使用了这些第三方库。
cum cum%
相比flat,cum则是这个函数内所有操作的物理耗时,比如包括了上述的step1、2、3、4。
cum%即是cum的时间/总运行时间。内存等参数同理。
一般cum是我们次关注的,且需要结合flat来看。flat可以让我们知道哪个函数耗时多,而cum可以帮助我们找到是哪些函数调用了这些耗时的(flat值大的)函数。
sum%
其上所有行的flat%的累加。可以视为,这一行及其以上行,其所有的directly操作一共占了多少物理时间。
3.2 内存字段解释
在Golang内存分析的过程中经常会出现很多内部相关内存字段,具体解释如下:
代码语言:shell复制Alloc uint64 //golang语言框架堆空间分配的字节数
TotalAlloc uint64 //从服务开始运行至今分配器为分配的堆空间总 和,只有增加,释放的时候不减少
Sys uint64 //服务现在系统使用的内存
Lookups uint64 //被runtime监视的指针数
Mallocs uint64 //服务malloc heap objects的次数
Frees uint64 //服务回收的heap objects的次数
HeapAlloc uint64 //服务分配的堆内存字节数
HeapSys uint64 //系统分配的作为运行栈的内存
HeapIdle uint64 //申请但是未分配的堆内存或者回收了的堆内存(空闲)字节数
HeapInuse uint64 //正在使用的堆内存字节数
HeapReleased uint64 //返回给OS的堆内存,类似C/C 中的free。
HeapObjects uint64 //堆内存块申请的量
StackInuse uint64 //正在使用的栈字节数
StackSys uint64 //系统分配的作为运行栈的内存
MSpanInuse uint64 //用于测试用的结构体使用的字节数
MSpanSys uint64 //系统为测试用的结构体分配的字节数
MCacheInuse uint64 //mcache结构体申请的字节数(不会被视为垃圾回收)
MCacheSys uint64 //操作系统申请的堆空间用于mcache的字节数
BuckHashSys uint64 //用于剖析桶散列表的堆空间
GCSys uint64 //垃圾回收标记元信息使用的内存
OtherSys uint64 //golang系统架构占用的额外空间
NextGC uint64 //垃圾回收器检视的内存大小
LastGC uint64 // 垃圾回收器最后一次执行时间。
PauseTotalNs uint64 // 垃圾回收或者其他信息收集导致服务暂停的次数。
PauseNs [256]uint64 //一个循环队列,记录最近垃圾回收系统中断的时间
PauseEnd [256]uint64 //一个循环队列,记录最近垃圾回收系统中断的时间开始点。
NumForcedGC uint32 //服务调用runtime.GC()强制使用垃圾回收的次数。
GCCPUFraction float64 //垃圾回收占用服务CPU工作的时间总和。如果有100个goroutine,垃圾回收的时间为1S,那么就占用了100S。
BySize //内存分配器使用情况