jinhailang / blog

技术博客:知其然,知其所以然
https://github.com/jinhailang/blog/issues
60 stars 6 forks source link

Go 程序性能分析过程 #51

Open jinhailang opened 5 years ago

jinhailang commented 5 years ago

Go 程序性能分析

性能分析的目的是发现程序瓶颈,为代码优化提供指导,以及对优化结果进行量化验证。主要从内存和 CPU 使用两个维度进行分析。 Go 官方提供了功能强大的包和工具 pprof。使用过程大致分成两步:

  1. 收集运行指标数据,生成 pprof 文件;
  2. 使用命令工具 go tool pprof 解析 pprof 文件,进行分析;

下面将详细阐述。

指标收集

有三种实践方式,前两种是在程序内(一般是 main 函数内)导入包 net/http/pprof,或者使用 runtime/pprof 包。 区别是前者提供 HTTP API 接口,可以直接在 Web 页面查看,而后者是在将运行数据写入到本地文件保存。一般都选择使用前者,因为使用更简单,使用 Web 交互也更方便,还可实时查看。

package main

import (
    "net/http"
    _ "net/http/pprof"
)

func main() {

    // some code

    http.ListenAndServe("127.0.0.1:6060", nil)
}

浏览器访问 http://127.0.0.1:6060/debug/pprof/,可以看到如下内容:

image

需要注意的是,可以指定请求参数 seconds 来收集指定时间内的数据,例如 CPU profile: go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30。 我们知道,这样生成的数据文件进行数据分析才更有意义,因为一般我们都是期望对程序某个运行时间段内进行分析。

还有一种方式是使用 go test 做性能测试,直接导出 pprof 文件:

go test -cpuprofile cpu.prof -memprofile mem.prof -bench .

很明显,这种方式更适合对程序某个模块(函数)做性能分析,上面两种方式用来做程序整体的性能分析。

数据分析

生成 pprof 文件后,有两种分析方式:命令交互式和 Web 图形式。两种方式都是使用命令工具 go tool pprof

image

image

结合前面“指标收集”阶段,可以使用 API 方式获取 pprof 数据,这里还可以直接使用 url 请求来分析,以分析 5 分钟内 CPU 使用情况为例:

go tool pprof -http="127.0.0.1:8081" main.go http://127.0.0.1:6060/debug/pprof/profile?seconds=300

这么做的好处是可以实时对 CPU,内存等维度进行分析,且可以任意切换维度。

火焰图

火焰图是常用的性能分析工具,非常直观。对于很多语言来说(比如 Lua)生成火焰图的过程是很繁琐的,要借助一堆工具。在 Go 1.11 之前一般也是借助开源工具 go-torch,不过 Go 1.11 对 go tool pprof 进行了功能增强,可以直接生成火焰图等图形,因此,go-torch 项目也已经废弃了。

y 轴表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。 x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它占有率更高,即执行的时间长或内存大。注意,x 轴不代表时间,而是所有的调用栈合并后,按字母顺序排列的。

火焰图就是看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。

小结

前面介绍了 Go 程序性能分析的过程,需要主要的是,只有对程序进行压测(程序达到瓶颈)的情况下,进行性能分析才有意义。 下面是工作中的一次实践过程 --- 程序压测与性能分析总结报告,作为参考。

参考

附:常用命令

jinhailang commented 5 years ago

Rule engine 程序压测报告

通过 kafka 消息对规则引擎(as-rule-engine)处理流程进行压测,计算最大 QPS,查找程序瓶颈,以及可优化的点。

核心处理流程图:

核心流程

压测的重点是上图中的 rule engine 模块,该模块是规则引擎核心模块,执行规则的匹配和结果计算。为了达到压测核心模块的目的,对入库(写 mysql)模块进行弱化,避免入库成为瓶颈。

测试环境

测试过程

首先,通过调节生产者生产消息速率,来间接控制规则引擎程序消息处理速率,从而到达分组压测目的。 rule engine 模块并发度可设置,通过设置不同的并发度,进行压测,压测分成四组:

测试步骤:

  1. 通过启动参数指定核心模块并发数,启动进程 as-rule-engine
  2. 设置 kafka 生产者消息写入速率阀值;
  3. 观察 kafka manger 统计页面,当 kafka topic 消息出现积压时,QPS 达到最大,停止压测;

测试结果:

序号 进程数 并发数 最大 QPS(k)
1 1 1 1.7
2 1 5 1.8
3 1 10 1.8
4 2 1 3.5

结果分析

首先,从测试结果可以发现,程序单机最大 QPS 在 1.8k 左右,而且核心模块的并发数对整个程序的性能影响很小。这就说明,程序测性能瓶颈不在核心模块,并且结合上面的流程图,可以推测,瓶颈在 kafka 消息处理(consumer) 模块。

其实,在第 4 组的测试时,在两个不同机器启动规则引擎程序,最大 QPS 结果约等于单进程时的 2 倍,就可以说明 QPS 与 消费者数量成正比。

为了进一步证明,通过 go pprof 工具获取规则引擎压测时的 CPU 火焰图。

cpu_flamegraph.zip

可以发现,在不考虑系统调用(runtime)的情况下,核心模块(Handle.func1)明显低于 consumer 模块耗费 CPU 时间。

PS: 上面的系统调用(runtime)占比也过高,应该主要是因为程序内使用了太多次锁,加解锁耗费了大量 CPU 时间。

end.