原创

go的性能分析:pprof工具

温馨提示:
本文最后更新于 2022年11月22日,已超过 512 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

pprof

pprof是GoLang程序性能分析工具,prof是profile(画像)的缩写 .通过pprof,我们可以得到程序执行的以下数据:

Profile Descriptions:

  • allocs:

    内存分配数据采样信息

  • block:

    导致同步原语阻塞的堆栈跟踪

  • cmdline:

    当前程序的命令行调用

    goroutine:

    所有当前goroutine的堆栈跟踪

  • heap:

    活动对象的内存分配采样。您可以指定gcGET参数以在获取堆样本之前运行gc。

  • mutex:

    争用互斥锁持有者的堆栈跟踪

  • profile:

    CPU配置文件。可以在秒GET参数中指定持续时间。获取配置文件后,使用go工具pprof命令调查配置文件。

  • threadcreate:

    导致创建新操作系统线程的堆栈跟踪

  • 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.

真正分析时常用4种

  • CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置

  • Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏

  • Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置

  • Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况

做性能分析,第一步需要先获取数据,然后对数据进行分析。所以下面展示一下如何进行数据获取。

数据获取方式

go的运行有2种情况,一种是常驻内存 服务式运行,例如http,tcp服务,一直运行接收请求,一种是工具脚本形式,运行完则退出

工具型应用

工具型应用调用比较简单,主要是用 runtime/pprof库,将画像数据写入文件中:

package main

import (
   "os"
   "runtime/pprof"
   _ "runtime/pprof"
)

func main() {
   //获取cpu profile
   cpuFile, _ := os.OpenFile("cpu.prof", os.O_CREATE|os.O_RDWR, 0644)
   defer cpuFile.Close()
   pprof.StartCPUProfile(cpuFile)
   defer pprof.StopCPUProfile()
   //获取内存profile
   memFile, _ := os.OpenFile("mem.prof", os.O_CREATE|os.O_RDWR, 0644)
   defer memFile.Close()
   pprof.WriteHeapProfile(memFile)

   //执行循环1000次
   for i := 0; i < 1000; i++ {
         
   }
}

执行完毕之后,可以直接看到cpu.prof和mem.prof文件,里面包含了画像数据:

(base) tioncico@appledeMacBook-Pro test % ls *.prof
cpu.prof        mem.prof

文件不能直接查看,需要通过go tool pprof 工具读取显示,在下面会讲到

服务型应用

服务型应用通过 "net/http/pprof"库进行获取,在http库中,默认使用了defaultServerMux,可以直接使用即可获取:

package main

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

func hello(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "hello world!")
}

func main() {
   http.HandleFunc("/", hello)              //设置访问的路由
   err := http.ListenAndServe(":8080", nil) //设置监听的端口
   if err != nil {
      fmt.Printf("ListenAndServe: %s", err)
   }
}

访问 http://localhost:8080  可以输出 hello world

访问 http://localhost:8080/debug/pprof/  可以查看到画像信息:

serverMux

可以看出,本身没有注册/debug/pprof,但是直接访问依然还是有数据的,那是因为在 http.ListenAndServe 时,没有传入自定义的serverMux进行路由处理,则pprof库自动进行了注册:

仙士可博客

如果你使用了自定义的serverMux,则需要自己注册,才能获取到pprof

http.HandleFunc("/debug/ppprof/", pprof.Index)

这样的话,访问 http://localhost:8080/debug/ppprof/ 也是有用的

开源框架

在不同的开源框架中,有提供自己封装好的pprof包,调用更加方便,具体使用请参考框架文档

pprof主要核心就是将pprof路由注册到服务中,并可以访问此服务即可

数据分析

数据分析通过命令  go tool pprof 进行,主要有2种查看模式

1:通过http路径进入交互模式

2:通过文件进入交互模式或者进入web页面查看

交互模式

进入命令行,基于http路径的数据分析:

(base) tioncico@appledeMacBook-Pro test % go tool pprof http://localhost:8080/debug/pprof/profile
Fetching profile over HTTP from http://localhost:8080/debug/pprof/profile
Saved profile in /Users/tioncico/pprof/pprof.samples.cpu.003.pb.gz
Type: cpu
Time: Nov 22, 2022 at 5:16pm (CST)
Duration: 30s, Total samples = 20ms (0.067%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

执行此命令后,会采集默认30秒之后的cpu数据,然后生成记录,进行数据分析,可以通过?=seconds=60改成60秒

也可以通过 go tool pprof cpu.prof(文件名) 方式进入

进入命令行交互模式之后,可以使用help查看所有命令,通过help <cmd|option> 查看子命令使用方法

(pprof) help
  Commands:
    callgrind        Outputs a graph in callgrind format
    comments         Output all profile comments
    disasm           Output assembly listings annotated with samples
    dot              Outputs a graph in DOT format
    eog              Visualize graph through eog
    evince           Visualize graph through evince
    gif              Outputs a graph image in GIF format
    gv               Visualize graph through gv
    kcachegrind      Visualize report in KCachegrind
    list             Output annotated source for functions matching regexp
    pdf              Outputs a graph in PDF format
    peek             Output callers/callees of functions matching regexp
    png              Outputs a graph image in PNG format
    proto            Outputs the profile in compressed protobuf format
    ps               Outputs a graph in PS format
    raw              Outputs a text representation of the raw profile
    svg              Outputs a graph in SVG format
    tags             Outputs all tags in the profile
    text             Outputs top entries in text form
    top              Outputs top entries in text form
    topproto         Outputs top entries in compressed protobuf format
    traces           Outputs all profile samples in text form
    tree             Outputs a text rendering of call graph
    web              Visualize graph through web browser
    weblist          Display annotated source in a web browser
    o/options        List options and their current values
    q/quit/exit/^D   Exit pprof

可以自行研究

通过top,可以查看到cpu占用最高的排序:

(pprof) top
Showing nodes accounting for 20ms, 100% of 20ms total
      flat  flat%   sum%        cum   cum%
      10ms 50.00% 50.00%       10ms 50.00%  runtime.libcCall
      10ms 50.00%   100%       20ms   100%  runtime.pthread_cond_wait
         0     0%   100%       20ms   100%  runtime.findrunnable
         0     0%   100%       20ms   100%  runtime.mPark
         0     0%   100%       20ms   100%  runtime.mcall
         0     0%   100%       20ms   100%  runtime.notesleep
         0     0%   100%       20ms   100%  runtime.park_m
         0     0%   100%       20ms   100%  runtime.schedule
         0     0%   100%       20ms   100%  runtime.semasleep
         0     0%   100%       20ms   100%  runtime.stopm
(pprof)

这个东西看不出啥,我们改造下http服务的helloworld,使其计算1000次快速排序:

package main

import (
   "fmt"
   "math/rand"
   "net/http"
   _ "net/http/pprof"
)

func hello(w http.ResponseWriter, r *http.Request) {
   //生成一个100个随机数字的数组
   arr := make([]int, 100)
   for i := 0; i < 100; i++ {
      arr[i] = rand.Intn(100)
   }
   for i := 0; i < 10000; i++ {
      //快速排序
      quickSort(arr, 0, len(arr)-1)
   }
   fmt.Fprintf(w, "hello world!")
}

func main() {
   http.HandleFunc("/", hello)              //设置访问的路由
   err := http.ListenAndServe(":8080", nil) //设置监听的端口
   if err != nil {
      fmt.Printf("ListenAndServe: %s", err)
   }
}

//快速排序
func quickSort(arr []int, left, right int) {
   if left < right {
      pivot := partition(arr, left, right)
      quickSort(arr, left, pivot-1)
      quickSort(arr, pivot+1, right)
   }
}

func partition(arr []int, left int, right int) int {
   pivot := arr[left]
   for left < right {
      for left < right && arr[right] >= pivot {
         right--
      }
      arr[left] = arr[right]
      for left < right && arr[left] <= pivot {
         left++
      }
      arr[right] = arr[left]
   }
   arr[left] = pivot
   return left
}

重新执行分析命令进行分析,top命令查看:

(pprof) top
Showing nodes accounting for 144.72s, 99.70% of 145.16s total
Dropped 21 nodes (cum <= 0.73s)
      flat  flat%   sum%        cum   cum%
   114.47s 78.86% 78.86%    114.83s 79.11%  main.partition
    30.03s 20.69% 99.55%    144.90s 99.82%  main.quickSort
     0.22s  0.15% 99.70%    117.12s 80.68%  main.hello
         0     0% 99.70%    114.42s 78.82%  net/http.(*ServeMux).ServeHTTP
         0     0% 99.70%    111.58s 76.87%  net/http.(*conn).serve
         0     0% 99.70%    115.71s 79.71%  net/http.HandlerFunc.ServeHTTP
         0     0% 99.70%    112.95s 77.81%  net/http.serverHandler.ServeHTTP
(pprof)

每一行都代表一个函数的信息,每列的标识为:

flat:函数在 CPU 上运行的时间

flat%:函数在CPU上运行时间的百分比

sum%:是从上到当前行所有函数累加使用 CPU 的比例,如第二行sum=48.52=28.79+19.73

cum:这个函数以及子函数运行所占用的时间,应该大于等于flat

cum%:这个函数以及子函数运行所占用的比例,应该大于等于flat%

最后一列:函数的名字

如果应用程序有性能问题,上面这些信息应该能告诉我们时间都花费在哪些函数的执行上

查看函数具体执行问题

通过 交互模式 list 函数名,即可查看到该函数的具体哪一行耗时:

(pprof)  list hello      
Total: 145.16s
ROUTINE ------------------------ main.hello in /Users/tioncico/go/src/test/server.go
     220ms    117.12s (flat, cum) 80.68% of Total
         .          .     11:   //生成一个100个随机数字的数组
         .          .     12:   arr := make([]int, 100)
         .          .     13:   for i := 0; i < 100; i++ {
         .          .     14:           arr[i] = rand.Intn(100)
         .          .     15:   }
     210ms      210ms     16:   for i := 0; i < 10000; i++ {
         .          .     17:           //快速排序
         .    116.90s     18:           quickSort(arr, 0, len(arr)-1)
         .          .     19:   }
      10ms       10ms     20:   fmt.Fprintf(w, "hello world!")
         .          .     21:}
         .          .     22:
         .          .     23:func main() {
         .          .     24:   http.HandleFunc("/", hello)              //设置访问的路由
         .          .     25:   err := http.ListenAndServe(":8080", nil) //设置监听的端口
(pprof)

可看到,for循环里面的quickSort耗时比较长

生成函数调用图

可以通过svg命令,生成一个svg文件,拖动到浏览器打开即可查看函数调用图,但是需要安装 graphviz 才可以使用,具体安装方法可以自行百度

mac安装方法:

brew install graphviz
go get -u github.com/ofabry/go-callvis

仙士可博客

内存画像

执行go tool pprof http://localhost:8080/debug/pprof/heap  即可,其他和cpu操作同理

web页面模式

通过-http 选项进入web页面:

go tool pprof  -http=:9091 http://localhost:8080/debug/pprof/profile 
go tool pprof  -http=:9091 文件名

仙士可博客

其他可以执行了解探索

正文到此结束
本文目录