Golang pprof 性能问题分析优化和实战经验

简介: Golang pprof 性能问题分析优化和实战经验

Golang pprof 性能问题分析优化和实战经验

go pprof 中各种 profiler 介绍

Go 自带了一个 pprof 的性能优化和分析的工具,这个工具包括 cpuprof 、memprof ,并且还提供了 Lookup 功能用于获取堆状态信息、线程状态信息、 goroutine 状态信息等。官方的博客有一篇文章介绍用法:《Profiling Go Programs》[1]

CPU profiler

CPU 的主频,即 CPU 内核工作的时钟频率(CPU Clock Speed)[2],主频的基本单位是赫兹(Hz),但更多的是以兆赫兹(MHz)或吉赫兹(GHz)为单位。时钟频率的倒数即为时钟周期,时钟周期的基本单位为秒(s),但更多的是以毫秒(ms)、微妙(us)或纳秒(ns)为单位。在一个时钟周期内,CPU 会执行一条运算指令;在 1000 Hz 的 CPU 主频下,每 1毫秒可以执行一条CPU运算指令;在1 MHz的CPU主频下,每1微妙可以执行一条CPU运算指令,以此类推。

在默认情况下,Go 语言的运行时系统会以 100 Hz 的的频率对 CPU 的使用情况进行取样,也就是说每秒取样 100 次,为什么使用这个频率呢?因为 100 Hz 既足够产生有用的数据,又不至于让系统产生停顿。在启动 CPU 使用情况记录操作之后,运行时系统就会以每秒100次的频率将取样数据写入到 CPU 概要文件中,通过pprof.StopCPUProfile 函数可以把 CPU 使用情况取样的频率设置为 0 从而来停止取样操作。

Memory profiler

Go 语言运行时系统会对用户应用程序运行期间的所有的堆内存分配进行记录,不管在取样的时候堆内存已用字节数是否有增长,只要有字节被分配且数量足够,内存分析器就会对其进行取样。runtime/pprof.WriteHeapProfile[3] 里面记录了相关使用方法。

Blocking Profile

goroutine 阻塞 profiler 展示 goroutine 阻塞等待同步原语(包括定时器channel),出现在代码中哪些地方。阻塞剖析器默认没有被开启,goroutine 阻塞的情况,可以通过 pprof 工具来查看,使用 net/http/pprof, runtime/pprof包,然后手动开启,程序中调用 runtime.SetBlockProfileRate,开启阻塞剖析器,SetBlockProfileRate 控制 blocking profile 中阻塞时间的报告粒度。

goroutine 阻塞的情况要根据业务的实际应用场景去分析,如果业务场景下就是那种需要耗时很久的逻辑,那么就是正常的,比如一些异步定时处理的耗时的任务。一般下线实时请求的服务,goroutine 阻塞的情况就要特别引起重视,避免因为是阻塞而导致协程泄漏,或者因为阻塞而消耗掉过多的 CPU 时间,从而使得正常服务逐步开始响应变慢。

Goroutine Profiler

goroutine 分析器可以分析并查看到进程里面所有活跃 goroutine 的当前堆栈,这个对于线上调试尤其是是阻塞、死锁等问题会十分方便。

关于 Goroutine Profiler 的分析结果,这里需要说明一下,只有显示 “syscall" 状态的 goroutine 才会消费掉我们的操作系统级别的线程,其他的状态比如 ”io wait“ 状态的 goroutine,或者其他状态的协程都不会消费掉我们的操作系统级别的线程,”io wait“ 状态的goroutine 是依附于非阻塞的网络轮询器上的。

go pprof 相关操作和命令介绍

go tool pprof

go pprof 支持在本地运行,但是一般我们很少这样使用,更多的通过 pprof 去分析线上环境的服务情况,也就是应用程序的真实的实时情况。

pprof 工具并不是用 Go 语言编写的,而是由 Perl 语言编写的, go tool pprof 命令会分析指定的概要文件并使得我们能够以交互式的方式访问其中的信息。但是光有概要文件还不够,我们还需要概要文件中信息的来源——命令源码文件的可执行文件。毕竟,概要文件中的信息是对在运行期间的用户程序取样的结果。而可以运行的Go语言程序只能是编译命令源码文件后生成的可执行文件。比如:

$ go tool pprof cpupprof cpupprof.prof
File: cpupprof
Type: cpu
Time: Dec 6, 2019 at 10:57am (CST)
Duration: 3s, Total samples = 0
No samples were found with the default sample value type.
Try "sample_index" command to analyze different sample values.
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 10
  • • cpupprof 就是我们的应用程序的二进制程序
  • • cpupprof.prof 就是这个程序所生成的 cpu prof 文件

更详细的用法和使用示例,可以继续往下看下面的演示过程。

net/http/pprof【推荐使用】

最常见的方法是使用 net/http/pprof,在我们的代码中运行一个 http server,然后远程通过 http 的请求来访问获取到相关数据,比如可以在浏览器里输入 http://localhost:6060/debug/pprof/goroutine?debug=2,就会看到 goroutine 的一些情况

svg 矢量图

通过 pprof 工具可以生成一个 svg 的矢量图,查看 svg 矢量图可以帮助我们确认代码的执行流程和调用情况。

svg 是使用 graphviz 生成的,我一般都是用 mac 电脑,在 mac 下可以直接通过 brew install graphviz 命令来安装;其他的系统,比如 centos下,我们可以通过 yum -y install graphviz来安装。

pprof 实战验证和验证

runtime/pprof【CPU profiling】

代码示例

cpupprof.go 源文件如下:

package main
import (
    "flag"
    "log"
    "os"
    "runtime/pprof"
    "sync"
    "time"
)
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go work(&wg)
    }
    time.Sleep(3 * time.Second)
    wg.Wait()
}
func work(wg *sync.WaitGroup) {
    var counter int
    for i := 0; i < 1000; i++ {
        time.Sleep(time.Millisecond * 100)
        counter++
    }
    wg.Done()
}

编译和 go tool pprof 运行分析

先编译 cpupprof.go 源文件:go build cpupprof.go ,得到 cpupprof 二进制文件

然后,运行 ./cpupprof -cpuprofile=cpupprof.prof

最后,再在同一个目录,执行 go tool pprof cpupprof cpupprof.prof 开始分析:

  • • top N :表示采样时间间隔内,耗时最长的 top N
  • • top5 -cum :cum 表示按第4和第5列排序
  • • web: 表示生成 svg 图
$ go tool pprof cpupprof cpupprof.prof
File: cpupprof
Type: cpu
Time: Dec 6, 2019 at 10:57am (CST)
Duration: 1.70mins, Total samples = 340ms ( 0.33%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 10
Showing nodes accounting for 340ms, 100% of 340ms total
Showing top 10 nodes out of 34
      flat  flat%   sum%        cum   cum%
     100ms 29.41% 29.41%      100ms 29.41%  runtime.pthread_cond_wait
      60ms 17.65% 47.06%       60ms 17.65%  runtime.nanotime
      60ms 17.65% 64.71%       60ms 17.65%  runtime.pthread_cond_signal
      50ms 14.71% 79.41%       50ms 14.71%  runtime.pthread_cond_timedwait_relative_np
      20ms  5.88% 85.29%       30ms  8.82%  runtime.notetsleep
      20ms  5.88% 91.18%       20ms  5.88%  runtime.usleep
      10ms  2.94% 94.12%       10ms  2.94%  runtime.pthread_mutex_unlock
      10ms  2.94% 97.06%      160ms 47.06%  runtime.semasleep
      10ms  2.94%   100%       10ms  2.94%  time.Sleep
         0     0%   100%       10ms  2.94%  main.work
(pprof) top5 -cum
Showing nodes accounting for 10ms, 2.94% of 340ms total
Showing top 5 nodes out of 34
      flat  flat%   sum%        cum   cum%
      10ms  2.94%  2.94%      160ms 47.06%  runtime.semasleep
         0     0%  2.94%      130ms 38.24%  runtime.mstart
         0     0%  2.94%      120ms 35.29%  runtime.mcall
         0     0%  2.94%      120ms 35.29%  runtime.park_m
         0     0%  2.94%      120ms 35.29%  runtime.schedule
(pprof) web
(pprof)


实战说明

一般而言,如果代码没有上线,那么可以在本地调试,然后我们可以本地编译、本地运行、本地调优。但是,如果代码已经上线运行,那么当然不能这样调试了,因此就需要用到 net/http/pprof包,通过 http 远程访问来调试

net/http/pprof

代码示例

package main
import (
    "flag"
    "log"
    "net/http"
    _ "net/http/pprof"
    "sync"
    "time"
)
func main() {
    flag.Parse()
    //这里实现了远程获取pprof数据的接口
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go work(&wg)
    }
    wg.Wait()
    // Wait to see the global run queue deplete.
    time.Sleep(3 * time.Second)
}
func work(wg *sync.WaitGroup) {
    time.Sleep(time.Second)
    mem := make([]int, 10000)
    log.Println(mem)
    var counter int
    for i := 0; i < 1e10; i++ {
        time.Sleep(time.Millisecond * 100)
        counter++
    }
    wg.Done()
}

http 访问查看和分析 pprof

通过网络访问来查看相关信息 http://localhost:6060/debug/pprof/

go tool pprof 运行分析

go tool pprof 查看 CPU 信息

go tool pprof http://localhost:6060/debug/pprof/profile 查看 CPU 信息:

$ go tool pprof http://localhost:6060/debug/pprof/profile
Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile
Saved profile in /Users/wudebao/pprof/pprof.samples.cpu.002.pb.gz
Type: cpu
Time: Dec 6, 2019 at 11:22am (CST)
Duration: 30s, Total samples = 90ms (  0.3%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 5
Showing nodes accounting for 90ms, 100% of 90ms total
Showing top 5 nodes out of 25
      flat  flat%   sum%        cum   cum%
      50ms 55.56% 55.56%       50ms 55.56%  runtime.pthread_cond_wait
      10ms 11.11% 66.67%       10ms 11.11%  runtime.nanotime
      10ms 11.11% 77.78%       10ms 11.11%  runtime.pthread_cond_signal
      10ms 11.11% 88.89%       10ms 11.11%  runtime.pthread_cond_timedwait_relative_np
      10ms 11.11%   100%       10ms 11.11%  runtime.usleep
(pprof) top5 -cum
Showing nodes accounting for 0, 0% of 90ms total
Showing top 5 nodes out of 25
      flat  flat%   sum%        cum   cum%
         0     0%     0%       60ms 66.67%  runtime.findrunnable
         0     0%     0%       60ms 66.67%  runtime.mcall
         0     0%     0%       60ms 66.67%  runtime.park_m
         0     0%     0%       60ms 66.67%  runtime.schedule
         0     0%     0%       60ms 66.67%  runtime.semasleep
(pprof)
(pprof)

go tool pprof 查看内存堆栈信息

go tool pprof http://localhost:6060/debug/pprof/heap 查看内存堆栈信息:

$ go tool pprof http://localhost:6060/debug/pprof/heap
Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in /Users/wudebao/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.004.pb.gz
Type: inuse_space
Time: Dec 6, 2019 at 11:33am (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 3692.74kB, 100% of 3692.74kB total
Showing top 10 nodes out of 17
      flat  flat%   sum%        cum   cum%
 1562.15kB 42.30% 42.30%  1562.15kB 42.30%  fmt.(*buffer).WriteByte
 1106.08kB 29.95% 72.26%  3180.24kB 86.12%  main.work
  512.50kB 13.88% 86.13%   512.50kB 13.88%  sync.(*Pool).pinSlow
  512.01kB 13.87%   100%   512.01kB 13.87%  reflect.packEface
         0     0%   100%  2074.16kB 56.17%  fmt.(*pp).doPrintln
         0     0%   100%  2074.16kB 56.17%  fmt.(*pp).printArg
         0     0%   100%  2074.16kB 56.17%  fmt.(*pp).printValue
         0     0%   100%  2074.16kB 56.17%  fmt.Sprintln
         0     0%   100%  2074.16kB 56.17%  log.Println
         0     0%   100%   512.50kB 13.88%  net/http.(*conn).readRequest
(pprof)
(pprof) list FindLoops
Total: 3.61MB
(pprof)

go tool pprof http://localhost:6060/debug/pprof/block

go tool pprof http://localhost:6060/debug/pprof/block 查看 goroutine blocking profile

go pprof 实战经验

常见 Profile 性能分析经验

关于 Profile 分析结果的一些经验,参考 Golang 核心开发者关于性能剖析[4]

  1. 1. 如果在 Profile 分析中,看到有大量时间花费在 runtime.mallocgc 函数中,那么我们的应用程序可能做了过多的小内存分配,并且 profile 还会告诉我们这些分配来自哪里,那么我们就可以根据这些结果来定位并优化我们的应用程序
  2. 2. 如果在 Profile 分析中,看到有大量时间花费在 channel sync.Mutex 、或者其他同步原语,或者系统组件上,那么我们的应用程序可能存在资源的竞争。这个时候,我们就需要去优化我们的代码架构,减少共享资源的访问。
  3. 3. 如果在 Profile 分析中,看到有大量时间花费在 syscall.Read/Write,那么我们的应用程序可能产生了太多小数据量读写操作,可考虑使用 bufio 来代替 os.File or net.Conn。
  4. 4. 如果在 Profile 分析中,看到有大量时间花费在 GC 上,要么是程序分配了太多的临时对象,要么是堆内存太小,导致垃圾收集操作运行频繁。

引入 net/http/pprof,暴露详细信息并采集

在我们的应用程序中,通过引入 net/http/pprof,然后可以提供详细的信息,包括堆栈信息,goroutine 数量等,这个在线上运行时,可以实时观察,并且最好能够结合 Prometheus 数据进行采集和监控。

对外暴露相关接口,示例使用如下:

// PProf
    profGroup := debugGroup.Group("/pprof")
    profGroup.GET("/", func(c *gin.Context) {
        pprof.Index(c.Writer, c.Request)
    })
    profGroup.GET("/goroutine", gin.WrapH(pprof.Handler("goroutine")))
    profGroup.GET("/block", gin.WrapH(pprof.Handler("block")))
    profGroup.GET("/heap", gin.WrapH(pprof.Handler("heap")))
    profGroup.GET("/threadcreate", gin.WrapH(pprof.Handler("threadcreate")))
    profGroup.GET("/profile", func(c *gin.Context) {
        pprof.Profile(c.Writer, c.Request)
    })
    profGroup.GET("/symbol", func(c *gin.Context) {
        pprof.Symbol(c.Writer, c.Request)
    })
    profGroup.GET("/trace", func(c *gin.Context) {
        pprof.Trace(c.Writer, c.Request)
    })

这些信息,有助于观察线上环境的运行时情况,比如 goroutine 当前数量、goroutine 有没有泄漏、内存有没有泄漏 or 暴增、CPU 最耗时的是哪个函数等。

引入 https://github.com/google/gops

gops 是由 Google 官方出品的一个命令行工具,与 ps 命令的功能类似,能够查看并诊断当前系统中 Go 程序的运行状态及内部情况。如果我们的应用程序引用了 gops 包,那么就可以针对 golang 的程序进行特定的分析,包括 CPU、MEM、 堆栈、Goroutine 、runtime、GC 等都可以搞定。

gops 和 http/pprof 我们可以任选一样来使用。

引用链接

[1] 《Profiling Go Programs》: https://blog.golang.org/profiling-go-programs

[2] CPU 的主频,即 CPU 内核工作的时钟频率(CPU Clock Speed): https://baike.baidu.com/item/%E4%B8%BB%E9%A2%91/103191

[3] runtime/pprof.WriteHeapProfile: https://golang.org/pkg/runtime/pprof/#WriteHeapProfile

[4] Golang 核心开发者关于性能剖析: https://my.oschina.net/evilunix/blog/371958


相关文章
|
7月前
|
Go 数据库
Golang 语言编写 gRPC 实战项目
Golang 语言编写 gRPC 实战项目
67 0
|
9天前
|
监控 Go 开发者
Golang深入浅出之-Goroutine泄漏检测与避免:pprof与debug包
【5月更文挑战第2天】本文介绍了Go语言并发编程中可能遇到的Goroutine泄漏问题,以及如何使用`pprof`和`debug`包来检测和防止这种泄漏。常见的问题包括忘记关闭channel和无限制创建goroutine。检测方法包括启动pprof服务器以监控Goroutine数量,使用`debug.Stack()`检查堆栈,以及确保每个Goroutine有明确的结束条件。通过这些手段,开发者可以有效管理Goroutine,维持程序性能。
33 7
|
10天前
|
Java Go
Golang深入浅出之-Goroutine泄漏检测与避免:pprof与debug包
【5月更文挑战第1天】本文介绍了Go语言中goroutine泄漏的问题及其影响,列举了忘记关闭通道、无限循环和依赖外部条件等常见泄漏原因。通过引入`net/http/pprof`和`runtime/debug`包,可以检测和避免goroutine泄漏。使用pprof的HTTP服务器查看goroutine堆栈,利用`debug`包的`SetGCPercent`和`FreeOSMemory`函数管理内存。实践中,应使用`sync.WaitGroup`、避免无限循环和及时关闭通道来防止泄漏。理解这些工具和策略对维护Go程序的稳定性至关重要。
23 4
|
10天前
|
JSON 监控 安全
Golang深入浅出之-Go语言中的反射(reflect):原理与实战应用
【5月更文挑战第1天】Go语言的反射允许运行时检查和修改结构,主要通过`reflect`包的`Type`和`Value`实现。然而,滥用反射可能导致代码复杂和性能下降。要安全使用,应注意避免过度使用,始终进行类型检查,并尊重封装。反射的应用包括动态接口实现、JSON序列化和元编程。理解反射原理并谨慎使用是关键,应尽量保持代码静态类型。
21 2
|
12天前
|
JSON JavaScript 前端开发
Golang深入浅出之-Go语言JSON处理:编码与解码实战
【4月更文挑战第26天】本文探讨了Go语言中处理JSON的常见问题及解决策略。通过`json.Marshal`和`json.Unmarshal`进行编码和解码,同时指出结构体标签、时间处理、omitempty使用及数组/切片区别等易错点。建议正确使用结构体标签,自定义处理`time.Time`,明智选择omitempty,并理解数组与切片差异。文中提供基础示例及时间类型处理的实战代码,帮助读者掌握JSON操作。
22 1
Golang深入浅出之-Go语言JSON处理:编码与解码实战
|
18天前
|
存储 测试技术 Go
Golang框架实战-KisFlow流式计算框架(2)-项目构建/基础模块-(上)
KisFlow项目源码位于&lt;https://github.com/aceld/kis-flow,初始阶段涉及项目构建和基础模块定义。首先在GitHub创建仓库,克隆到本地。项目目录包括`common/`, `example/`, `function/`, `conn/`, `config/`, `flow/`, 和 `kis/`。`go.mod`用于包管理,`KisLogger`接口定义了日志功能,提供不同级别的日志方法。默认日志对象`kisDefaultLogger`打印到标准输出。
603 1
Golang框架实战-KisFlow流式计算框架(2)-项目构建/基础模块-(上)
|
23天前
|
存储 监控 Go
Golang框架实战-KisFlow流式计算框架(1)-概述
KisFlow是针对缺乏数仓平台但又有实时计算需求的企业的解决方案,它提供分布式批量消费、有状态流式计算、数据流监控和分布式任务调度等功能。通过KisFunction实现业务逻辑复用,减轻对业务数据库的压力。系统包括流式计算层和任务调度层,支持多种数据源和中间件集成。KisConfig用于配置管理,KisFunction是基本计算单元。设计目标是使业务工程师能轻松进行流式计算。项目源码可在GitHub查看:https://github.com/aceld/kis-flow。
48 0
Golang框架实战-KisFlow流式计算框架(1)-概述
|
3月前
|
Go
Golang拼接字符串性能对比
【2月更文挑战第8天】Golang拼接字符串性能对比
45 2
|
3月前
|
监控 数据可视化 数据挖掘
Golang性能分析神器:pprof与火焰图实战揭秘
在软件开发的世界里,性能分析如同一把锋利的剑,它能帮助开发者洞悉程序的运行状态,发现并解决那些隐藏在代码深处的性能瓶颈。而在Go语言的生态系统中,pprof无疑是这把剑中的佼佼者。本文将带你深入了解pprof的使用方法,并通过火焰图这一直观的工具,让你对性能分析有一个全新的认识。
|
3月前
|
缓存 Cloud Native 测试技术
Golang 乐观锁实战:Gorm 乐观锁的优雅使用
在现代软件开发中,数据一致性是一个永恒的话题。随着系统规模的扩大和并发操作的增加,如何有效地处理并发冲突,确保数据的完整性,成为了开发者必须面对的挑战。本文将带你深入了解 Golang 中 Gorm ORM 库的乐观锁机制,并通过实际示例,展示如何在项目中优雅地使用乐观锁。