GoLang-pprof-案例实践及解析

前言

pprof是GoLang中最常用的性能分析工具,本篇文章主要来聊聊pprof该怎么用

1 实践Demo

1.1 前置准备

github.com/wolfogre/go... 是非常经典 go pprof 学习案例,本文将直接引用该项目作为性能分析的实战素材.

环境准备:使用Linux系统,安装好 graphviz(pprof图形化展示时依赖的软件)

复制代码
sudo apt install graphviz

Mac用户可以通过brew安装graphviz

arduino 复制代码
brew install graphviz // 安装graphviz
dot -V // 验证是否安装成功

观察一下 go-pprof-practice 的 main 函数,其中有几个关键的地方:

  • 匿名导入了 net/http/pprof pkg
  • 调用 runtime.SetMutexProfileFraction 和 runtime.SetBlockProfileRate,启用 block 和 mutex 性能分析(默认是关闭的)
  • 异步启动默认的 http server(http.DefaultServerMux,与pprof联动)
  • 循环调用一系列 animal 的 live 方法(里面已经提前埋设好一系列的性能炸弹,等待使用 pprof 将之一一逮捕)
go 复制代码
package main
​
import (
    "log"
    "net/http"
    // 启用 pprof 性能分析
    _ "net/http/pprof"
    "os"
    "runtime"
    "time"
​
    "github.com/wolfogre/go-pprof-practice/animal"
)
​
func main() {
    // ...
​
    runtime.GOMAXPROCS(1)
    // 启用 mutex 性能分析
    runtime.SetMutexProfileFraction(1)
    // 启用 block 性能分析
    runtime.SetBlockProfileRate(1)
​
    go func() {
        // 启动 http server. 对应 pprof 的一系列 handler 也会挂载在该端口下
        if err := http.ListenAndServe(":6060", nil); err != nil {
            log.Fatal(err)
        }
        os.Exit(0)
    }()
​
    // 运行各项动物的活动
    for {
        for _, v := range animal.AllAnimals {
            v.Live()
        }
        time.Sleep(time.Second)
    }
}

启动项目

go 复制代码
go run main.go

1.2 pprof页面总览

进入 http/pprof 页面:(端口与启动的 http server 一致)

bash 复制代码
http://localhost:6060/debug/pprof/ 

页面中包含各项内容,我们重点关注下面几项指标,下文中逐一展开分析:

  • profile:探测各函数对 cpu 的占用情况
  • heap:探测内存分配情况
  • block:探测阻塞情况 (包括 mutex、chan 等)
  • mutex:探测互斥锁占用情况
  • goroutine:探测协程使用情况

1.3 CPU分析

cpu 分析是在一段时间内进行打点采样,通过查看采样点在各个函数栈中的分布比例,以此来反映各函数对 cpu 的占用情况.

点击页面上的 profile 后,默认会在停留 30S 后下载一个 cpu profile 文件.

通过交互式指令打开文件后,查看 cpu 使用情况:

go 复制代码
go tool  pprof {YOUR PROFILE PATH}
erlang 复制代码
(pprof) top
Showing nodes accounting for 13230ms, 99.77% of 13260ms total
Dropped 12 nodes (cum <= 66.30ms)
      flat  flat%   sum%        cum   cum%
   12690ms 95.70% 95.70%    13230ms 99.77%  github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat
     540ms  4.07% 99.77%      540ms  4.07%  runtime.asyncPreempt
         0     0% 99.77%    13230ms 99.77%  github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Live
         0     0% 99.77%    13250ms 99.92%  main.main
         0     0% 99.77%    13250ms 99.92%  runtime.main
(pprof) 

信息拆解:

  • 12690 ms ------ 采样点大约覆盖了 12690 ms 的时长

  • flat:某个函数执行时长(只聚焦函数本身,剔除子函数部分)

    • 12690 ms ------ Tiger.Eat 这个方法本身的调用时长
  • flat%:某个函数执行时长(只聚焦函数本身,剔除子函数部分)

  • sum%:某个函数及其之上父函数的总时长占比

  • cum:某个函数及其子函数的总调用时长

    • 13230ms ------ Tiger.Eat 加上其调用子函数 runtime.asyncPreempt 的总时长
  • cum%:某个函数及其子函数的调用时长在总时长中的占比

看看 cpu 分析流程,其实现原理是:

  • 创建一个 timer,定时向 go 进程中的各个 thread 发送信号
  • thread 接收到信号后,会将记录当前函数栈信息
  • 通过一个异步 goroutine 持续接收函数栈信息,将其写入到 cpu profile 文件返回给用户

除此之外,还可以通过图形化界面来展示 cpu profile 文件中的内容:

ini 复制代码
go tool pprof -http=:8082  {YOUR PROFILE PATH}

如下图所示,在调用链的拓扑结构中,几项指标是和上述所介绍的内容一一对应的:

此外,如果对于火焰图使用比较习惯,这里也可以启用火焰图的格式: VIEW -> Flame Graph

在 CPU 性能分析中,要定位性能瓶颈可以核心看 flat% 这个指标,在这个案例中不难看出问题症结产生于 Tiger.Eat 函数,我们打开项目代码一探究竟:

go 复制代码
func (t *Tiger) Eat() {
    log.Println(t.Name(), "eat")
    loop := 10000000000
    for i := 0; i < loop; i++ {
        // do nothing
    }
}

可以看到,作者在这里埋了个炸弹,通过 for 循环大量空转打满 CPU.

另外,这里我们主要注意到另一个细节,是 pprof 告诉我们 Tiger.Eat 中还有个子函数 runtime.asyncPreempt 花费了大约 540 ms 的时间,但是这一点在代码中并没有体现,这又是怎么回事呢?

这里我们需要简单一下在 golang 中关于 goroutine 超时抢占机制的设定:

  • 监控线程:在 go 进程启动时,会启动一个 monitor 线程,作为第三方观察者角色不断轮询探测各 g 的执行情况,对于一些执行时间过长的 g 出手干预

    • 协作式抢占:当 g 在运行过程中发生栈扩张时(通常由函数调用引起),则会触发预留的检查点逻辑,查看自己若是因为执行过长而被 monitor 标记,则会主动让渡出 m 的执行权
    • 在 Tiger.Eat 方法中,由于只是简单的 for 循环空转无法走到检查点,因此这种协作式抢占无法生效

    • 非协作式抢占:在 go 1.14 之后,启用了基于信号量实现的非协作抢占机制. Monitor 探测到 g 超时会发送抢占信号,g 所属 m 收到信号后,会修改 g 的 栈程序计数器 pc 和栈顶指针 sp 为其注入 asyncPreempt 函数. 这样 g 会调用该函数完成 m 执行权的让渡
go 复制代码
// 此时执行方是即将要被抢占的 g,这段代码是被临时插入的逻辑
func asyncPreempt2() {
    gp := getg()
    gp.asyncSafePoint = true
    // mcall 切换至 g0,然后完成 g 的让渡
    mcall(gopreempt_m)
    gp.asyncSafePoint = false
}

我在之前发布的文章:温故知新---Golang GMP 万字洗髓经 5.3 小节中对有关 g 超时抢占相关内容展开了详细的分析,大家感兴趣的话可以展开了解.

1.4 heap分析

下面是关于内存的分析流程,点击 heap 进入 http://localhost:6060/debug/pprof/heap?debug=1

在页面的路径中能看到 debug 参数,如果 debug = 1,则将数据在页面上呈现;如果将 debug 设为 0,则会将数据以二进制文件的形式下载,并支持通过交互式指令或者图形化界面对文件内容进行呈现. block/mutex/goroutine 的机制也与此相同,后续章节中不再赘述.

从页面中获取到有关 heap 的信息

先看内容的第一行:

yaml 复制代码
heap profile: 2: 2583691264 [138: 8338153472] @ heap/1048576

内容含义是在全局视角下的一些信息:

  • 2---活跃对象个数
  • 2583691264---活跃对象大小(单位 byte)
  • 21---历史至今所有对象个数
  • 3371171968---历史至今所有对象总计大小(byte)
  • 1048576---内存采样频率(约每 M 采样一次)

再看下面的内容:

less 复制代码
1: 1291845632 [1: 1291845632] @ 0x102d2dcb4 0x102d2d568 0x102d2de28 0x102b1d2e8 0x102b5c114
#        0x102d2dcb3        github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Steal+0xf3        /Users/bytedance/go/src/go-pprof-practice/animal/muridae/mouse/mouse.go:60
#        0x102d2d567        github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Live+0x47        /Users/bytedance/go/src/go-pprof-practice/animal/muridae/mouse/mouse.go:25
#        0x102d2de27        main.main+0xb7                                                                        /Users/bytedance/go/src/go-pprof-practice/main.go:31
#        0x102b1d2e7        runtime.main+0x287                                                                /Users/bytedance/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.darwin-arm64/src/runtime/proc.go:272

对应为某个函数栈中的信息:

  • 1-该函数栈上当前存活的对象个数
  • 1291845632-当前存活对象总大小(byte)
  • \] 内的内容也表示历史至今,不再赘述

arduino 复制代码
func (m *Mouse) Steal() {
    log.Println(m.Name(), "steal")
    max := constant.Gi
    for len(m.buffer)*constant.Mi < max {
       m.buffer = append(m.buffer, [constant.Mi]byte{})
    }
}

1.5 block分析

下面进行阻塞分析,首先明确block分析的含义:

查看某个 goroutine 陷入 waiting 状态(被动阻塞,通常因 gopark 操作触发,比如因加锁、读chan条件不满足而陷入阻塞)的触发次数和持续时长.

pprof 默认不启用 block 分析,若要开启则需要进行如下设置:

scss 复制代码
runtime.SetBlockProfileRate(1)

此处的入参能够控制 block 采样频率:

  • 1:始终采用
  • <=0:不采样
  • 1:当阻塞时长(ns)大于该值则采样,否则有阻塞时长/rate的概率被采样

下面点击页面中的 block,进入 http://localhost:6060/debug/pprof/block?debug=1 查看阻塞信息:

shell 复制代码
--- contention:
cycles/second=1000000000
​
206303883866 206 @ 0x102ae9734 0x102d2cbf0 0x102d2c818 0x102d2de28 0x102b1d2e8 0x102b5c114
#        0x102ae9733        runtime.chanrecv1+0x13                                                                /Users/bytedance/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.darwin-arm64/src/runtime/chan.go:489
#        0x102d2cbef        github.com/wolfogre/go-pprof-practice/animal/felidae/cat.(*Cat).Pee+0x9f        /Users/bytedance/go/src/go-pprof-practice/animal/felidae/cat/cat.go:39
#        0x102d2c817        github.com/wolfogre/go-pprof-practice/animal/felidae/cat.(*Cat).Live+0x37        /Users/bytedance/go/src/go-pprof-practice/animal/felidae/cat/cat.go:19
#        0x102d2de27        main.main+0xb7                                                                        /Users/bytedance/go/src/go-pprof-practice/main.go:31
#        0x102b1d2e7        runtime.main+0x287                                                                /Users/bytedance/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.darwin-arm64/src/runtime/proc.go:272
  • cycles/second=1000002977------是每秒钟对应的cpu周期数. pprof在反映block时长时,以cycle为单位
  • 206303883866------阻塞的cycle数. 可以换算成秒:206303883866/1000002977 ≈ 206s
  • 206------发生的阻塞次数

于是我们定位到其中一处引起阻塞的代码是 Cat.Pee,每当函数被调用时会简单粗暴地等待 timer 1S,里面会因读 chan 而陷入阻塞:

scss 复制代码
func (c *Cat) Pee() {
    log.Println(c.Name(), "pee")
​
    <-time.After(time.Second)
}

1.6 mutex分析

mutex 分析看的是某个 goroutine 持有锁的时长(mutex.Lock -> mutex.Unlock 之间这段时间),且只有在存在锁竞争关系时才会上报这部分数据.

pprof 默认不开启 mutex 分析,需要显式打开开关:

scss 复制代码
runtime.SetMutexProfileFraction(1)

入参控制的是 mutex 采样频率:

  • 1------始终进行采样
  • 0------关闭不进行采样
  • <0------不更新这个值,只是把之前设的值结果读出来
  • 1 ------有 1/rate 的概率下的事件会被采样

点击 mutex 进入 http://localhost:6060/debug/pprof/mutex?debug=1 页面查看信息:

shell 复制代码
--- mutex:
cycles/second=1000000000
sampling period=1
250337419543 250 @ 0x102d2c714 0x102d2c6d5 0x102b5c114
#        0x102d2c713        sync.(*Mutex).Unlock+0x73                                                                /Users/bytedance/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.darwin-arm64/src/sync/mutex.go:225
#        0x102d2c6d4        github.com/wolfogre/go-pprof-practice/animal/canidae/wolf.(*Wolf).Howl.func1+0x34        /Users/bytedance/go/src/go-pprof-practice/animal/canidae/wolf/wolf.go:58
  • 1000000000 ------ 每秒下的 cycle 数
  • 250337419543 ------ 持有锁的 cycle 总数
  • 250 ------ 采样了 250 次

于是定位到占有锁较多的方法是 Wolf.Howl,每次加锁后都睡了一秒:

scss 复制代码
func (w *Wolf) Howl() {
    log.Println(w.Name(), "howl")
​
    m := &sync.Mutex{}
    m.Lock()
    go func() {
        time.Sleep(time.Second)
        m.Unlock()
    }()
    m.Lock()
}

1.7 goroutine分析

最后针对 goroutine 进行分析,点击 goroutine 进入http://localhost:6060/debug/pprof/goroutine?debug=1页面获取信息:

shell 复制代码
goroutine profile: total 48
40 @ 0x102b53d88 0x102b57f10 0x102d2c7b8 0x102b5c114
#        0x102b57f0f        time.Sleep+0xdf                                                                                /Users/bytedance/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.darwin-arm64/src/runtime/time.go:300
#        0x102d2c7b7        github.com/wolfogre/go-pprof-practice/animal/canidae/wolf.(*Wolf).Drink.func1+0x27        /Users/bytedance/go/src/go-pprof-practice/animal/canidae/wolf/wolf.go:34
​
4 @ 0x102b53d88 0x102b57f10 0x102d2d9b0 0x102b5c114
#        0x102b57f0f        time.Sleep+0xdf                                                                                /Users/bytedance/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.darwin-arm64/src/runtime/time.go:300
#        0x102d2d9af        github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Pee.func1+0x2f        /Users/bytedance/go/src/go-pprof-practice/animal/muridae/mouse/mouse.go:43

先看第一行:

yaml 复制代码
goroutine profile: total 48

total 173------总计有 173 个 goroutine

然后能够定位到几个创造 goroutine 数量较大的方法:

scss 复制代码
40 @ 0x102b53d88 0x102b57f10 0x102d2c7b8 0x102b5c114
#        0x102b57f0f        time.Sleep+0xdf                                                                                /Users/bytedance/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.darwin-arm64/src/runtime/time.go:300
#        0x102d2c7b7        github.com/wolfogre/go-pprof-practice/animal/canidae/wolf.(*Wolf).Drink.func1+0x27        /Users/bytedance/go/src/go-pprof-practice/animal/canidae/wolf/wolf.go:34
​
4 @ 0x102b53d88 0x102b57f10 0x102d2d9b0 0x102b5c114
#        0x102b57f0f        time.Sleep+0xdf                                                                                /Users/bytedance/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.8.darwin-arm64/src/runtime/time.go:300
#        0x102d2d9af        github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Pee.func1+0x2f        /Users/bytedance/go/src/go-pprof-practice/animal/muridae/mouse/mouse.go:43
func (w *Wolf) Drink() {
    log.Println(w.Name(), "drink")
    for i := 0; i < 10; i++ {
        go func() {
            time.Sleep(30 * time.Second)
        }()
    }
}
func (m *Mouse) Pee() {
    log.Println(m.Name(), "pee")
    go func() {
        time.Sleep(time.Second * 30)
        max := constant.Gi
        for len(m.slowBuffer)*constant.Mi < max {
            m.slowBuffer = append(m.slowBuffer, [constant.Mi]byte{})
            time.Sleep(time.Millisecond * 500)
        }
    }()
}

最后来到 goroutine 分析流程,比较简单,直接取得 g 的数量并且遍历各个 g 的栈信息即可:

至此,我们把 pprof 中常用的性能分析流程串联了一遍,实战 demo 到此为止.

相关推荐
懒猫爱上鱼1 分钟前
Android 四大组件与 AMS 交互的完整对比
面试
华清远见成都中心14 分钟前
嵌入式工程师技术面试有哪些注意事项?
面试·职场和发展
沐雪架构师18 分钟前
大模型Agent面试精选15题(第三辑)LangChain框架与Agent开发的高频面试题
面试·职场和发展
appearappear33 分钟前
Mac 上重新安装了Cursor 2.2.30,重新配置 springboot 过程记录
java·spring boot·后端
xiaoxue..42 分钟前
React 之 Hooks
前端·javascript·react.js·面试·前端框架
谷哥的小弟1 小时前
Spring Framework源码解析——RequestContext
java·后端·spring·框架·源码
武子康1 小时前
Java-205 RabbitMQ 工作模式实战:Work Queue 负载均衡 + fanout 发布订阅(手动ACK/QoS/临时队列)
java·性能优化·消息队列·系统架构·rabbitmq·java-rabbitmq·mq
IT_陈寒1 小时前
Vite 5大优化技巧:让你的构建速度飙升50%,开发者都在偷偷用!
前端·人工智能·后端
www_stdio1 小时前
爬楼梯?不,你在攀登算法的珠穆朗玛峰!
前端·javascript·面试
风止何安啊1 小时前
🚀别再卷 Redux 了!Zustand 才是 React 状态管理的躺平神器
前端·react.js·面试