文章目录

  • 0.前言
  • 1.什么是 pprof
  • 2.pprof 的作用是什么
  • 3.pprof 的使用模式
  • 4.安装 Graphviz
  • 4.应用程序性能分析
    • 4.1 CPU 性能分析
    • 4.2 内存性能分析
      • 4.2.1 生成 profile
      • 4.2.2 分析 profile
  • 5.HTTP 服务性能分析
    • 5.1 CPU 性能分析
    • 5.2 内存性能分析
  • 6.小结
  • 参考文献

0.前言

有时,我们开发的 Golang 程序会出现 CPU 使用率达到 100%,内存使用量过大,死锁等问题,我们该如何定位上诉问题的具体位置,来解决程序的到性能问题呢?

1.什么是 pprof

Go 是一个非常注重性能的语言,语言内置了里性能分析库 runtime/pprof、net/http/pprof 和配套的分析工具 go tool pprof。

所以我们平时说的 golang pprof 实际上包含两部分:
(1)编译到程序中的 runtime/pprof 和 net/http/pprof 包;
(2)性能分析工具 go tool pprof。

其中 runtime/pprof 和 net/http/pprof 区别如下:
(1)runtime/pprof 对于只跑一次的程序,例如每天只跑一次的离线预处理程序,调用 pprof 包提供的函数,手动开启性能数据采集。
(2)net/http/pprof 是对 runtime/pprof 的封装,封装成接口对外提供网络访问。对于一个 HTTP 服务,访问 pprof 提供的 HTTP 接口,获得性能数据。

如果你的程序或者服务遇到了性能问题,诉诸 pprof 就对了。

2.pprof 的作用是什么

根据其名字我们就知道 pprof 是用于性能分析,找到程序或服务的性能瓶颈,来优化提升程序或服务的性能。

利用 pprof,我们一般用来分析程序如下几种数据:

类型 描述
allocs A sampling of all past memory allocations
block Stack traces that led to blocking on synchronization primitives
cmdline The command line invocation of the current program
goroutine Stack traces of all current goroutines
heap A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.
mutex Stack traces of holders of contended mutexes
profile CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.
threadcreate Stack traces that led to the creation of new OS threads
trace A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.

最常分析的是 CPU profile 和 heap profile 这两个性能数据。

3.pprof 的使用模式

使用 pprof 有几种不同的方法。

(1)报告生成(Report generation)

pprof 可以生成 DOT 格式的图形报告,并使用 Graphviz 工具将其转换为多种格式。如果提示 Graphviz 没有安装,则需要安装。

命令格式:

pprof <format> [options] source

支持的 format 有:

-dot: 生成 DOT 格式的报告。所有其他格式均由该格式转换得到
-svg: 生成 SVG 格式的报告
-web: 生成一个 SVG 格式的临时报告,并启动 web 浏览器查看该报告
-png, -jpg, -gif, -pdf: 生成对应格式的报告

(2)交互式终端使用(Interactive terminal use)

输入命令直接进入命令行交互模式:

pprof [options] source

进入命令行交互模式后,比如输入子命令 help 可以查看帮助信息,输入 top -cum 根据累计权重对要展示的信息(如函数 CPU 耗时)进行排序。

(3)Web 界面(Web interface)

pprof 开启在指定端口上的 HTTP 服务,使用浏览器访问对应端口的 url 便可以查看性能报告。

Web 界面交互方式比较直观,是最常用的交互方式。

pprof -http=[host]:[port] [options] source

端口号随便填,但不要与现有程序的端口冲突。

4.安装 Graphviz

Graphviz(Graph Visualization Software)是一个开源的图形可视化软件,它可以将 pprof 生成的性能文件转换为我们人类可读的图形,比如函数调用关系图和火焰图。

访问 Graphviz 官网 下载所需版本(Windows,Linux,Mac等)进行安装。

我这里选择的是 Windows 版本,下文性能分析也在 Windows 10 环境下完成。

4.应用程序性能分析

4.1 CPU 性能分析

CPU 性能分析主要是查看函数占用 CPU 的时长,来判断程序主要耗时部分是哪里。这也是我们最常分析程序性能的手段。

只需要调用 runtime/pprof 库即可得到我们想要的数据。

假设我们实现了这么一个程序,随机生成了 5 组数据,并且使用冒泡排序法排序。

package mainimport ("math/rand""time"
)func generate(n int) []int {rand.Seed(time.Now().UnixNano())nums := make([]int, 0)for i := 0; i < n; i++ {nums = append(nums, rand.Int())}return nums
}
func bubbleSort(nums []int) {for i := 0; i < len(nums); i++ {for j := 1; j < len(nums)-i; j++ {if nums[j] < nums[j-1] {nums[j], nums[j-1] = nums[j-1], nums[j]}}}
}func main() {n := 10for i := 0; i < 5; i++ {nums := generate(n)bubbleSort(nums)n *= 10}
}

如果我们想度量这个应用程序的 CPU 性能数据,只需要在 main 函数中添加两行代码即可:

import ("math/rand""os""runtime/pprof""time"
)func main() {pprof.StartCPUProfile(os.Stdout)defer pprof.StopCPUProfile()n := 10for i := 0; i < 5; i++ {nums := generate(n)bubbleSort(nums)n *= 10}
}

为了简单,直接将数据输出到标准输出 os.Stdout。运行该程序,将输出定向到文件 cpu.pprof 中。

go run main.go > cpu.pprof

一般来说,不建议将结果直接输出到标准输出,因为如果程序本身有输出,则会相互干扰,直接记录到一个文件中是最好的方式。

func main() {f, _ := os.OpenFile("cpu.pprof", os.O_CREATE|os.O_RDWR, 0644)defer f.Close()pprof.StartCPUProfile(f)defer pprof.StopCPUProfile()n := 10for i := 0; i < 5; i++ {nums := generate(n)bubbleSort(nums)n *= 10}
}

这样只需运行 go run main.go 即可。

得到 CPU 性能数据后,我们开始使用 go tool pprof 工具进行分析。

我们采用 Web 界面交互方式来查看性能报告,命令行输入:

go tool -http=:9999 cpu.pprof

会自动打开浏览器,我们将看到下面这样的页面:


通过上面的函数调用关系图,可以看到 main.bubbleSort 是消耗 CPU 最多的函数。

我们也可以选择使用火焰图(Flame Graph)来查看。

火焰图中,每一块代表一个函数,越大代表占用 CPU 的时间越长。从上到下,表示主调函数与被调函数。同时它也支持点击块深入进行分析!

找到了性能瓶颈,我们就可以对症下药。比如将排序算法改为复杂度为 O(nlogn) 的快排来提高程序性能。

4.2 内存性能分析

4.2.1 生成 profile

假设我们实现了这么一个程序,生成长度为 N 的随机字符串,拼接在一起。

package mainimport ("github.com/pkg/profile""math/rand"
)const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"func randomString(n int) string {b := make([]byte, n)for i := range b {b[i] = letterBytes[rand.Intn(len(letterBytes))]}return string(b)
}func concat(n int) string {var s stringfor i := 0; i < n; i++ {s += randomString(n)}return s
}func main() {defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()concat(1000)
}

接下来,我们使用一个易用性更强的库 pkg/profile 来采集性能数据,pkg/profile 封装了 runtime/pprof 的接口,使用起来更简单。

比如我们想度量 concat() 的 CPU 性能数据,只需要一行代码即可生成 profile 文件。

import ("github.com/pkg/profile"
)func main() {defer profile.Start().Stop()concat(100)
}

运行 go run main.go:

go run main.go
2020/11/22 18:38:29 profile: cpu profiling enabled, C:\Users\dablelv\AppData\Local\Temp\profile068616584\cpu.pprof
2020/11/22 18:39:12 profile: cpu profiling disabled,C:\Users\dablelv\AppData\Local\Temp\profile068616584\cpu.pprof

CPU profile 文件已经在 tmp 目录生成,得到 profile 文件后,就可以像之前一样,用 go tool pprof 命令,在浏览器或命令行进行分析了。

接下来将使用类似的方式采集内存数据,同样地,只需简单地修改 main() 函数即可。

func main() {defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()concat(1000)
}

运行程序:

go run main.go
2021/05/31 19:05:48 profile: memory profiling enabled (rate 1), C:\Users\dablelv\AppData\Local\Temp\profile768395255\mem.pprof
2021/05/31 19:05:48 profile: memory profiling disabled, C:\Users\dablelv\AppData\Local\Temp\profile768395255\mem.pprof

4.2.2 分析 profile

接下来,我们在浏览器中分析内存性能数据:

go tool pprof -http=:9999 C:\Users\dablelv\AppData\Local\Temp\profile768395255\mem.pprof

会自动打开浏览器,我们将看到下面这样的页面:

从这张图中,我们可以看到 concat 消耗了 524KB 内存,randomString 仅消耗了 22KB 内存。理论上,concat 函数仅仅是将 randomString 生成的字符串拼接起来,消耗的内存应该和 randomString 一致,但怎么会产生 20+ 倍的差异呢?

这和 Go 字符串内存分配的方式有关系。字符串是不可变的,因为将两个字符串拼接时,相当于是产生新的字符串,如果当前的空间不足以容纳新的字符串,则会申请更大的空间,将新字符串完全拷贝过去,这消耗了 2 倍的内存空间。在这 100 次拼接的过程中,会产生多次字符串拷贝,从而消耗大量的内存。

那有什么好的方式呢?使用 strings.Builder 替换 + 进行字符串拼接,将有效地降低内存消耗。

func concat(n int) string {var s strings.Builderfor i := 0; i < n; i++ {s.WriteString(randomString(n))}return s.String()
}

接下来,重新运行程序:

go run main.go
2021/05/31 20:22:12 profile: memory profiling enabled (rate 1), C:\Users\dablelv\AppData\Local\Temp\profile592131615\mem.pprof
2021/05/31 20:22:12 profile: memory profiling disabled, C:\Users\dablelv\AppData\Local\Temp\profile592131615\mem.pprof

这次换个方式来使用 pprof,直接通过命令行进入交互:


可以看到,使用 strings.Builder 后,concat 内存消耗降为了原来的 1/8 。

5.HTTP 服务性能分析

如果我们的程序不是跑一次就结束的程序,而是一个常驻的 HTTP 服务。那么开启 pprof 的方式和上面介绍的会有所区别。

我们可以利用 Go 提供的 net/http/pprof 包来收集性能数据。只需要在代码中通过匿名方式 import _ net/http/pprof 包就行了。

5.1 CPU 性能分析

下面我们稍微改造一下上面随机数排序,生成指定范围内的随机数并排序。以 HTTP 接口的形式来调用,并获取其 CPU profile 文件。

package mainimport ("fmt""math/rand""net/http""time"_ "net/http/pprof"
)func generate(n int) []int {rand.Seed(time.Now().UnixNano())nums := make([]int, 0, n)for i := 0; i < n; i++ {nums = append(nums, rand.Intn(n))}return nums
}// bubbleSort 升序排序
func bubbleSort(nums []int) {for i := 0; i < len(nums); i++ {for j := 1; j < len(nums)-i; j++ {if nums[j] < nums[j-1] {nums[j], nums[j-1] = nums[j-1], nums[j]}}}
}func randHandler(w http.ResponseWriter, r *http.Request) {nums := generate(1000)bubbleSort(nums)fmt.Fprintf(w, "ordered random number is %v", nums)
}func main() {http.HandleFunc("/Rand", randHandler)http.ListenAndServe(":8888", nil)
}

上面写了一个简单 HTTP 服务,将生成随机有序数字的函数注册到路径 “/Rand”。启动服务后,直接在浏览器访问:http://127.0.0.1:8888/Rand,就可以获取排序后的随机数。

同样的做法,当我们在代码中 import 包 “net/http/pprof” 时,pprof 包会自动注册 handler,处理相关的请求:

// src/net/http/pprof/pprof.gofunc init() {http.Handle("/debug/pprof/", http.HandlerFunc(Index))http.Handle("/debug/pprof/cmdline", http.HandlerFunc(Cmdline))http.Handle("/debug/pprof/profile", http.HandlerFunc(Profile))http.Handle("/debug/pprof/symbol", http.HandlerFunc(Symbol))http.Handle("/debug/pprof/trace", http.HandlerFunc(Trace))
}

所以我们访问路径 “/debug/pprof/” 会得到一个汇总页面:


可以直接点击上面的链接进入子页面,查看相关的汇总信息。

关于 goroutine 的信息有两个链接,goroutine 和 full goroutine stack dump,前者是一个汇总的消息,可以查看 goroutines 的总体情况,后者则可以看到每一个 goroutine 的状态。

点击 profile 和 trace 则会在后台进行一段时间的数据采样,采样完成后,返回给浏览器一个 profile 文件,之后在本地通过 go tool pprof 工具进行分析。

当我们下载得到 CPU profile 文件后,执行命令:

go tool pprof -http=:9999 profile

或者生成 svg 格式的函数调用关系图报告,通过浏览器来查看:

go tool pprof profile
web


从上图可以看到,CPU 耗时主要花在了冒泡排序函数,我们可以采用性能更高的排序算法,比如快排来优化。

注意,采集 CPU profile 期间,我们要不断触发对接口 randHandler 的调用,不然采集不到数据。

5.2 内存性能分析

同样地,我们将上面的内存有问题的代码改造成 HTTP 接口进行采样分析。

package mainimport ("fmt""math/rand""net/http""strings"_ "net/http/pprof"
)const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"func randomString(n int) string {b := make([]byte, n)for i := range b {b[i] = letterBytes[rand.Intn(len(letterBytes))]}return string(b)
}func concatSmall(n int) string {var s strings.Builderfor i := 0; i < n; i++ {s.WriteString(randomString(n))}return s.String()
}func concatLarge(n int) string {var s stringfor i := 0; i < n; i++ {s += randomString(n)}return s
}func concatHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "random string is %v", concatLarge(1000))
}func main() {debug.SetGCPercent(-1)http.HandleFunc("/concat", concatHandler)http.ListenAndServe(":8888", nil)
}

注意,使用 debug.SetGCPercent(-1) 关闭自动 GC,不然会采集不到内存信息。

启动服务后,直接在浏览器访问 http://127.0.0.1:8888/concat,就可以获取拼接后的随机字符。

同样地,我们在命令行直接访问 http://127.0.0.1:8888/debug/pprof/heap,通过 Web 页面交互方式进行内存分析。

go tool pprof -http=:9999 http://127.0.0.1:8888/debug/pprof/heap

在堆信息中你可以查看分配的堆大小和对象数量,或者当前没有释放的堆大小和对象数量。

或者通过命令行查看:

go tool pprof http://127.0.0.1:8888/debug/pprof/heap


下面我们换成 concatSmall 函数进行拼接字符串,再测试一次。

func concatHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "random string is %v", concatSmall(100))
}

还是通过命令行交互方式进行分析:

可见 concatSmall 占用更小的内存。

6.小结

pprof 是进行 Go 程序性能分析的有力工具,它通过采样、收集运行中的 Go 程序性能相关的数据,生成 profile 文件。之后,提供三种不同的展现形式,让我们能更直观地看到相关的性能数据。

本文介绍了 Go 程序性能分析工具 pprof 的使用,以应用程序和 HTTP 服务两种不同类型的程序为例,介绍 CPU 和内存性能数据的生成与分析。

其他类型的性能分析不再赘述,比如 Block Profiling,Mutex Profiling。后面如果在实践中遇到,会补充。


参考文献

[1] Go 语言高性能编程.pprof 性能分析
[2] golang pprof 实战
[3] 煎鱼.Golang 大杀器之性能剖析 PProf
[4] google pprof doc
[5] 深度解密Go语言之 pprof
[6] Profiling Go programs with pprof

白话 Golang pprof相关推荐

  1. golang pprof

    这里填写标题 1. golang pprof 1.1. pprof 实例 2. go tool 2.1. `--inuse/alloc_space` `--inuse/alloc_objects` 区 ...

  2. 使用golang pprof进行性能分析

    golang pprof,说实话自己还一次都没有实际操作过. 最近这几天的需求恰好需要分析下一个看似很简单的服务,内存配置上限是900m,最终在大量并发的时候出现oom的情况. 代码准备 首先代码需要 ...

  3. Golang pprof简介

    目录 概要 pprof的作用 使用方式 交互式常用命令 以profile为例,其余的指标也是用一样的命令 Top N List func Traces web func Base Debug=[num ...

  4. golang pprof工具

    pprof工具 pprof是什么 pprof是分析和显示性能相关数据的工具 pprof读取profile.proto格式的分析抽样集合数据,同时创建报告来展现和帮助分析数据,它能创建包括文本和图型报告 ...

  5. Golang pprof 使用

    目录 什么是 Profile? 两种收集方式 工具型应用 服务型应用 go tool ppof 获取和分析 profile 数据 终端 可视化 什么是 Profile? 在计算机性能调试领域里,pro ...

  6. 白话 Golang 协程池

    文章目录 1.何为并发 2.并发的好处 3.Go 如何并发 4.G-P-M 调度模型 5.Go 程的代价 6.协程池的作用 7.简易协程池的设计&实现 8.开源协程池的使用 9.小结 参考文献 ...

  7. Golang pprof 性能分析与火焰图

    文章目录 1. 安装graphviz 1.1 下载 graphviz (windows 环境) 1.2 测试graphviz是否安装成功 2. 使用pprof 2.1 修改代码 2.2 火焰图生成 3 ...

  8. 一看就懂系列之Golang的pprof

    前言 这是一篇给网友的文章,正好最近在研究分析golang的性能,我觉得是时候来一个了断了. 正文 1.一句话简介 Golang自带的一款开箱即用的性能监控和分析工具. (全篇看的过程中没必要特意记忆 ...

  9. Golang相关面试题

    Golang (63条消息) go/golang面试中的高频八股文问题_光哥2020的博客-CSDN博客_golang八股文 1.go的profile工具? profile就是定时采样,收集cpu,内 ...

最新文章

  1. Spring干货汇总(含Spring Boot与Spring Cloud)
  2. python读取大文件-Python如何读取、拆分大文件
  3. python 写入csv文件固定列_将元组列表写入csv文件保持列一致
  4. java 修改ip_如何用脚本快速修改IP地址(Netsh)
  5. 阿加莎•克里斯蒂作品04东方快车谋杀案
  6. mysql历史日志文件_MySQL 历史 binlog 日志处理
  7. java的oauth2.0_[转]Java的oauth2.0 服务端与客户端的实现
  8. 十八般武艺玩转GaussDB(DWS)性能调优:Plan hint运用
  9. python制作软件excel_利用Python制作一个 截图+Excel操作浏览器小工具
  10. 手把手教你写个ORM(一)
  11. FL Studio软件隐藏优惠码分享,音乐制作必备,创作无限可能!
  12. linux下select/poll/epoll机制的比较
  13. 泰山游记:所为非风光,为历史尔
  14. c#实现txt转化为excel
  15. 房地产开发项目管理浅析
  16. 计算机技术是不是信息技术,计算机技术和信息技术
  17. android 4.4 surfaceflinger 渲染,Android4.4.3--surfaceflinger导致系统起不来,ldb显示问题...
  18. 我来读代码之三(d-podium)
  19. java关注列表_如何从一个Instagram帐户中获取关注者列表?
  20. python画位势高度图_气候变化位势高度

热门文章

  1. 黑客泄露50多万服务器、路由器和物联网设备的密码
  2. 夜上海音乐播放器 v 1.0
  3. C# 套接字编程:Scoket,我用Scoket做的C# Windows应用程序如下:
  4. python之twisted模块安装
  5. thrift之TTransport层的堵塞的套接字I/O传输类TSocket
  6. Linux命令学习手册-gpg命令
  7. ubuntu安装ssh无法连接解决日志(已解决,可连接)-转
  8. Mac Safari浏览器的阅读列表与iPhone、iPad (iOS)不同步的问题
  9. [Java] 蓝桥杯ALGO-113 算法训练 数的统计
  10. [Python] L1-040. 最佳情侣身高差 团体程序设计天梯赛GPLT