聊聊Golang性能分析工具pprof的使用

 更新时间:2023年05月25日 11:46:06   作者:洛小妍  
对于线上稳定运行的服务来说, 可能会遇到 cpu、mem 利用率升高的问题,那我们就需要使用 pprof 工具来进行性能分析,所以本文就来和大家讲讲pprof的具体使用吧

对于线上稳定运行的服务来说, 可能会遇到 cpu、mem 利用率升高的问题。有时可能增加机器配置或重启服务也解决不了问题。那我们就需要使用 pprof 工具来进行性能分析, 找出造成 cpu、mem 升高的问题。

pprof

pprof 是 golang 的性能分析工具。pprof 通过读取 profile.proto 格式的性能采样数据来生成相关报告。从而分析服务中存在的性能问题。

pprof profiles

存储采样的数据集合。在 golang 中, 采样数据分别包含这几种类型:

cpu

cpu 采样是最常用的采用类型。当开始 cpu 采样时, 每隔 10ms 会进行一次采样, 记录当前运行 goroutines 的堆栈信息。当采样完成之后, 可以分析哪些代码比较消耗 cpu。

memory

memory 采样主要用来分析服务的堆内存的分配和回收。程序运行时, 栈内存是可以直接分配和回收的。因此, 无需采集栈内存的使用情况。

block

采集阻塞数据。主要用于定位 channel 所导致的 goroutines 阻塞。

mutex

主要用来采集锁竞争相关数据, 分析锁竞争导致的系统的延迟。

生成 profile 文件

在开始进行性能相关分析之前, 需要生成对应的采样文件。然后通过 pprof 工具对 profile 文件进行分析, 找出影响性能的代码。在 golang 中, 可以使用以下三种方式生成 profile 文件:

测试函数

在使用基准测试时, 可以指定参数生成对应的 profile 文件。

go test -cpuprofile {proflie_name} -bench .

http 访问

对于持续运行的任务, 例如:http、rpc。可以导入 net/http/pprof 来注册 debug 路由, 生成对应的 proflie 文件。对于非 http 的服务, 需要注册一个 debug 端口。

直接生成 profile 文件

对于非持续性任务。例如:定时任务。可以通过指定方法生成对应的 proflie 文件。

import (
   "github.com/pkg/profile" // 对 runtime/pprof 进行的封装
   "fmt"
)
func main() {
   defer func() {
      profile.Start(profile.CPUProfile, profile.ProfilePath("."))
   }()
   func() {
      fmt.Println(5 + 6)
   }()
}

分析 profile 文件

下面将通过具体的例子, 来尝试分析对应的 cpu、mem 等指标。

cpu 分析

在下面的例子中, 尝试分析哪个函数比较占用 cpu.

package main
import (
   "log"
   "net/http"
   _ "net/http/pprof"
   "runtime"
   "time"
)
//go:noinline
func req(num int64) {
   A(num)
   B(num)
}
//go:noinline
func B(num int64) {
   for i := int64(0); i < num/2; i++ {
   }
}
//go:noinline
func A(num int64) {
   for i := int64(0); i < num; i++ {
   }
}
func main() {
   runtime.GOMAXPROCS(1)
   go func() {
      log.Println(http.ListenAndServe("localhost:3016", nil))
   }()
   for {
      time.Sleep(time.Second / 4)
      go func() {
         req(time.Now().Unix())
      }()
   }
}

下载并保存对应的 cpu profile, 采样时间 19s:

curl ``http://127.0.0.1:3016/debug/pprof/profile``?seconds=19 --output cpu.pprof

启动可视化界面:

go tool pprof -http=:8080 cpu.pprof

从最右侧可以看出, 此次采集共耗时 19.21s, 采样得到的 cpu 时间为 16.73s 「87.10%」。由于是采样, 所以 total samples 是小于 Duration 的。通过 view 来切换不同的试图。

Top, 用于展示不同函数的耗时占比。Flat: 当前函数运行耗时。Flat%: Flat 对应的占比。Sum%: 累积使用占比。Cum: 当前函数及子函数运行耗时。Cum%: Cum 对应的占比。

对于下面的例子:统计到的 cpu 时间为 16.73s, 函数 A 本身耗时 13.3s 「79.49%」; 函数 A 及其子函数耗时为 13.85s「82.78%」; 函数 req 本身耗时 0; 子函数耗时 16.73s「100%」。

Graph, 以图的形式展示不同函数的耗时占比。如下图所示:函数入口为 main.func2「匿名函数」; 0 of 16730ms「匿名函数不占用任何资源, cpu 采样总时长为 16730ms」; main.req 为 main.func2 的子函数「函数自身不占用任何资源」; main.A「13850ms 函数自身占用 13300ms」 和 mian.B「2880ms 函数自身占用 2810ms」 为 main.req「16730ms」 的子函数。

通过 Graph 可以直观的看到哪些函数占用的资源比较高, 方框越大资源占比越高。同时可以直观的看到调用关系。

Flame Graph, 火焰图。通过火焰图可以看到各个函数的耗时, 以及子函数的耗时。横轴越长占用的资源越多, 纵轴越长, 调用层级越深。

Source, 查看对应的源码。在下图中, req 的子函数 A 和 B 共占用了 100% 的资源。在函数 B 中, 函数自身耗时占用了 2.81s, 其中 for 循环语句占用 2.18s。

mem 分析

下面是一段内存分配的代码。heap 采样并不需要像 cpu 采样一样确定固定周期。heap 关注的是内存的分配和回收, 只需采集对应的事件即可。

package main
import (
   "fmt"
   "log"
   "net/http"
   _ "net/http/pprof"
   "runtime"
   "time"
)
//go:noinline
func req() {
   buf = append(buf, allocate()...)
}
//go:noinline
func allocate() []byte {
   var b []byte
   for i := 0; i < 1024; i++ {
      temp := make([]byte, 1024)
      b = append(b, temp...)
   }
   return b
}
var buf []byte
func main() {
   runtime.GOMAXPROCS(1)
   go func() {
      log.Println(http.ListenAndServe("localhost:3016", nil))
   }()
   for {
      time.Sleep(time.Second)
      go func() {
         req()
         fmt.Println(len(buf))
      }()
   }
}

下载堆内存采样文件。

curl ``http://127.0.0.1:3016/debug/pprof/heap`` --output mem.pprof

使用 webui 打开文件。

go tool pprof -http=:8080 mem.pprof

共分为四种采样类型:alloc_objects: 所有分配的对象; alloc_space: 所有分配的空间; inuse_objects: 活跃的对象; inuse_space: 活跃的空间。这里需要注意的是, inuse_space 使用的空间是要小于当前进程使用的系统空间的「使用中的, gc 未释放给操作系统的, cgo 申请的空间」

通过 alloc 可以分析哪里在频繁的分配空间, inuse 可以用来分析内存没有释放。切到 inuse_sapce, 可以看到 req 函数以及它的子函数 allocate 在不断分配空间。

goroutine 分析

下面是一个 goroutine 泄漏的例子。

package main
import (
   "fmt"
   "log"
   "net/http"
   _ "net/http/pprof"
   "runtime"
   "time"
)
//go:noinline
func req() {
   c := make(chan struct{})
   tick := time.NewTicker(time.Millisecond)
   go func() {
      time.Sleep(time.Millisecond * 2)
      c <- struct{}{}
   }()
   select {
   case <-c:
      return
   case <-tick.C:
      return
   }
}
func main() {
   runtime.GOMAXPROCS(1)
   go func() {
      log.Println(http.ListenAndServe("localhost:3016", nil))
   }()
   for {
      time.Sleep(time.Second / 10)
      go func() {
         req()
      }()
      fmt.Println(runtime.NumGoroutine())
   }
}

下载堆 goroutine 采样文件。

curl ``http://127.0.0.1:3016/debug/pprof/``goroutine --output goroutine.pprof

使用 webui 打开文件。

go tool pprof -http=:8080 goroutine.pprof

切到火焰图, 可以看到 main.req.func1「匿名函数」占用了需要 goroutine。我们切到 Souce 定位到代码行数。可以看到, 卡在了往非缓存的 channel 写数据。由于接收端已经 return, 以此 goroutine 等待写入而无法释放。我们只需声明一个带缓冲的 channel 就可以解决问题。

总结

本文简单介绍了 golang 的性能分析工具。如何进行性能采样, 查看采样的数据并对其进行分析。介绍了 Graph、Flame Graph、Source、Top 的用法。最后, 通过几个简单的例子来进行 cpu、mem、goroutine 的分析。

其实, 在真实的的线上服务中, 如果没有遇到具体的问题「接口变慢、cpu 或 mem 持续升高、goroutine 泄漏、死锁」, 不太建议过度的进行优化。倒不如花精力聚焦在业务思考上。

以上就是聊聊Golang性能分析工具pprof的使用的详细内容,更多关于Golang pprof的资料请关注脚本之家其它相关文章!

相关文章

  • 使用golang如何优雅的关机或重启操作示例

    使用golang如何优雅的关机或重启操作示例

    这篇文章主要为大家介绍了使用golang如何优雅的关机或重启操作示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04
  • 使用Gomock进行单元测试的方法示例

    使用Gomock进行单元测试的方法示例

    这篇文章主要介绍了使用Gomock进行单元测试的方法示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • 详解Go语言中结构体与JSON间的转换

    详解Go语言中结构体与JSON间的转换

    这篇文章主要为大家详细介绍了Go语言中结构体与JSON间的转换,文中的示例代码讲解详细,对学习Go语言有一定的帮助,需要的可以参考一下
    2022-12-12
  • Go语言的文件名、标识符、关键字和包基础教程

    Go语言的文件名、标识符、关键字和包基础教程

    Go的关键字不能被用作标识符,这是一个重要的限制,以避免命名冲突和语法混淆,这篇文章主要给大家介绍了关于Go语言文件名、标识符、关键字和包的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-06-06
  • 解决goland 导入项目后import里的包报红问题

    解决goland 导入项目后import里的包报红问题

    这篇文章主要介绍了解决goland 导入项目后import里的包报红问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • 使用go的interface案例实现多态范式操作

    使用go的interface案例实现多态范式操作

    这篇文章主要介绍了使用go的interface案例实现多态范式操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • go variant底层原理深入解析

    go variant底层原理深入解析

    这篇文章主要为大家介绍了go variant底层原理深入解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • go语言简单的处理http请求的函数实例

    go语言简单的处理http请求的函数实例

    这篇文章主要介绍了go语言简单的处理http请求的函数,实例分析了Go语言处理http请求的技巧,需要的朋友可以参考下
    2015-03-03
  • Go语言字符串常见操作的使用汇总

    Go语言字符串常见操作的使用汇总

    这篇文章主要为大家总结了Go语言中常见的几种字符串操作,例如:位置索引、替换、统计次数等,文中的示例代码讲解详细,感兴趣的可以了解一下
    2022-04-04
  • Go语言如何利用Mutex保障数据读写正确

    Go语言如何利用Mutex保障数据读写正确

    这篇文章主要介绍了互斥锁的实现机制,以及 Go 标准库的互斥锁 Mutex 的基本使用方法,文中的示例代码讲解详细,需要的小伙伴可以参考一下
    2023-05-05

最新评论