golang 调试分析方法

前言

文章首发于Noah Sun’s Home golang 调试分析方法,转载请注明出处,谢谢!

断点调试

Visual Studio Code

创建配置

VSCode IDE要运行或者调试golang程序之前,需要创建运行配置 launch.json ,方法如下:
vscode_config_add

打开菜单“运行”->“添加配置”,在创建的配置添加如下内容:

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Package",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "cwd": "${workspaceFolder}",
            "program": "${workspaceFolder}/main.go",
            "args": []
        }
    ]
}

配置说明:
cwd:当前工作目录,可以使用变量 ${workspaceFolder} 。
program:可执行程序或者文件。
args:程序运行所需参数,数组类型。

更多配置的用法请参考 visualstudio debugging

调试方法

设置断点,如下图所示:
vscode_breakpoint_add

使用快捷键F5,或者打开菜单,点击“运行”->“启动调试”,调试主界面如下,这样就可以愉快的调试了。
vscode_debug_main

GoLand

创建配置

打开菜单,“Run”->“Debug”
goland_config_add

在弹出框选“Edit Configurations”
goland_config_select

编辑配置如下图所示,编辑完后点击Apply应用即可完成。
goland_debug_config

调试方法

设置断点,如下图所示
goland_breakpoint_add

启动debug,MAC快捷键control+option+D,或者点击菜单“Run”->“Debug”,选择之前创建的配置,即可调试,如下图所示:
goland_debug_main

性能分析

pprof

pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,包括CPU、内存、goroutine、锁等。golang标准库提供了以下两种使用方式:

  • net/http/pprof:采集 HTTP Server 的运行时数据进行分析
  • runtime/pprof:采集程序(含Server和非Server)的运行数据进行分析

另外,golang也提供了比较多好用的可视化工具来负责开发者做分析,使用方式包括报告的生成、交互式终端和Web界面。

pprof工具准备

# 安装pprof工具
go install github.com/google/pprof@latest

# 安装图形化依赖工具 Graphviz
# 以Mac为例,其他平台安装方法请参考 https://graphviz.org/download/
brew install Graphviz

# Mac平台如果安装有报错,比如
==> Pouring jasper-3.0.2.arm64_monterey.bottle.tar.gz
Error: No such file or directory @ rb_sysopen - /Users/xxx/Library/Caches/Homebrew/downloads/d3af18f496d6e7cae9775e1e69ea79074687e03221367f075c3a6e6f2c77c705--jasper-3.0.2.arm64_monterey.bottle.tar.gz

# 可以手工逐个安装报错的依赖,比如
brew install jasper
brew install gdk-pixbuf
brew install pango
brew install librsvg

分析使用

本例使用的完整 demo 放在 go-learing 的 examples/pprof 目录下。

标准库自带Web界面
使用方法

简单的demo

package main

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

func main() {
        log.Printf("pprof demo is running")
        http.ListenAndServe("0.0.0.0:8090", nil)
}

本例子使用标准库提供的 “net/http/pprof” 包,通过import该包后,HTTP服务程序就会注入 /debug/pprof 相关的路由,可以访问 http://localhost:8090/debug/pprof/ 查看总览,如下所示

/debug/pprof/

Types of profiles available:
Count        Profile
3        allocs
0        block
0        cmdline
4        goroutine
3        heap
0        mutex
0        profile
7        threadcreate
0        trace

下面是各种 profile 的描述:

  • 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. 堆栈当前所有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. 活动对象的内存分配抽样。在获取堆样本之前,可以指定gc GET参数来运行gc。
  • 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. CPU profile。可以在GET参数中指定以秒为单位的持续时间,默认是30s。在获得profile文件之后,使用go工具pprof命令来研究它。
  • 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. 当前程序执行的痕迹。可以在GET参数中指定以秒为单位的持续时间。在获得trace文件之后,使用go工具 trace 命令来研究它。
pprof工具的交互式终端
使用方法
# 方法一
go tool pprof ${source}

# 方法二,前提是要安装 pprof ,具体方法请查看“ pprof 工具准备”章节
pprof ${source}

source可以是通过"net/http/pprof" 包提供的http url ,也可以是本地 profile 文件。

cpu分析
$ go tool pprof http://localhost:8090/debug/pprof/profile?seconds=15

Fetching profile over HTTP from http://localhost:8090/debug/pprof/profile?seconds=15
Saved profile in /Users/xxx/pprof/pprof.samples.cpu.004.pb.gz
Type: cpu
Time: Apr 5, 2022 at 3:13pm (CST)
Duration: 15s, Total samples = 290ms ( 1.93%)
Entering interactive mode (type "help" for commands, "o" for options)
(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

执行该命令后,需等待seconds参数指定的时间(默认30s),采样完成后,即可用交互式命令进行分析,具体可以使用 help 指令查看说明,比如 top 可以查看占用 cpu 最多的 n 个记录。

(pprof) top
Showing nodes accounting for 290ms, 100% of 290ms total
      flat  flat%   sum%        cum   cum%
     280ms 96.55% 96.55%      280ms 96.55%  github.com/rukesun/go-learning/examples/pprof/internal/simulator.(*XXCPU).Run
      10ms  3.45%   100%       10ms  3.45%  runtime.memmove
         0     0%   100%       10ms  3.45%  github.com/rukesun/go-learning/examples/pprof/internal/simulator.(*XXHeap).Run
         0     0%   100%       10ms  3.45%  runtime.growslice
  • flat:该位置的运行耗时
  • flat%:该位置的 CPU 运行耗时总比例
  • sum%:该位置累积使用 CPU 总比例
  • cum:该位置加上它的子调用运行总耗时
  • cum%:该位置加上它的的子调用的 CPU 运行耗时总比例

最后一列为函数调用的位置。

(pprof) help list
Output annotated source for functions matching regexp
  Usage:
    list<func_regex|address> [-focus_regex]* [-ignore_regex]*
    Include functions matching func_regex, or including the address specified.
    Include samples matching focus_regex, and exclude ignore_regex.

(pprof) list Run
Total: 300ms
ROUTINE ======================== github.com/rukesun/go-learning/examples/pprof/internal/simulator.(*XXCPU).Run in /Users/ht/research/golang/go-learning/examples/pprof/internal/simulator/xxcpu.go
     270ms      300ms (flat, cum)   100% of Total
         .          .     16:}
         .          .     17:
         .          .     18:func (c *XXCPU) Run() {
         .          .     19:        log.Printf("%v Run", c.Name())
         .          .     20:        loop := 1000000000
     270ms      300ms     21:        for i := 0; i < loop; i++ {
         .          .     22:                // do nothing
         .          .     23:        }
         .          .     24:}

通过list命令可以查看关联的代码,比如这里可以看出XXCPU的Run方法里面,有个循环的代码会消耗较多的 cpu 。

内存分析

$ go tool pprof http://localhost:8090/debug/pprof/heap

(pprof) top
Showing nodes accounting for 36353.94kB, 100% of 36353.94kB total
Showing top 10 nodes out of 20
      flat  flat%   sum%        cum   cum%
   32768kB 90.14% 90.14%    32768kB 90.14%  github.com/rukesun/go-learning/examples/pprof/internal/simulator.(*XXHeap).Run
 2048.81kB  5.64% 95.77%  2048.81kB  5.64%  runtime.malg
 1025.12kB  2.82% 98.59%  1025.12kB  2.82%  runtime.allocm
     512kB  1.41%   100%      512kB  1.41%  runtime.doaddtimer
         0     0%   100%      512kB  1.41%  runtime.mcall
         0     0%   100%      512kB  1.41%  runtime.modtimer
         0     0%   100%  1025.12kB  2.82%  runtime.mstart
         0     0%   100%  1025.12kB  2.82%  runtime.mstart0
         0     0%   100%  1025.12kB  2.82%  runtime.mstart1
         0     0%   100%  1025.12kB  2.82%  runtime.newm
(pprof) list Run
Total: 35.50MB
ROUTINE ======================== github.com/rukesun/go-learning/examples/pprof/internal/simulator.(*XXHeap).Run in /Users/xxx/research/golang/go-learning/examples/pprof/internal/simulator/xxheap.go
      32MB       32MB (flat, cum) 90.14% of Total
         .          .     17:}
         .          .     18:
         .          .     19:func (h *XXHeap) Run() {
         .          .     20:        log.Printf("%v Run", h.Name())
         .          .     21:        for i := 0; i < 10; i++ {
      32MB       32MB     22:                h.data = append(h.data, [1024 * 1024]byte{})
         .          .     23:        }
         .          .     24:        log.Printf("After %v Run, len:%v, cap:%v", h.Name(), len(h.data), cap(h.data))
         .          .     25:}

同样通过top和list大法,找出内存分配较多的地方。

协程分析
$ go tool pprof http://localhost:8090/debug/pprof/goroutine

Fetching profile over HTTP from http://localhost:8090/debug/pprof/goroutine
Saved profile in /Users/xxx/pprof/pprof.goroutine.001.pb.gz
Type: goroutine
Time: Apr 5, 2022 at 3:24pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)

(pprof) top
Showing nodes accounting for 3002, 100% of 3003 total
Dropped 27 nodes (cum <= 15)
      flat  flat%   sum%        cum   cum%
      3002   100%   100%       3002   100%  runtime.gopark
         0     0%   100%       3000 99.90%  github.com/rukesun/go-learning/examples/pprof/internal/simulator.(*XXGoroutine).Run.func1
         0     0%   100%       3000 99.90%  time.Sleep

(pprof) list Run
Total: 3003
ROUTINE ======================== github.com/rukesun/go-learning/examples/pprof/internal/simulator.(*XXGoroutine).Run.func1 in /Users/xxx/research/golang/go-learning/examples/pprof/internal/simulator/xxgoroutine.go
         0       3000 (flat, cum) 99.90% of Total
         .          .     18:
         .          .     19:func (g *XXGoroutine) Run() {
         .          .     20:        log.Printf("%v Run", g.Name())
         .          .     21:        for i := 0; i < 1000; i++ {
         .          .     22:                go func() {
         .       3000     23:                        time.Sleep(3600 * time.Second)
         .          .     24:                }()
         .          .     25:        }
         .          .     26:}
ROUTINE ======================== runtime/pprof.writeRuntimeProfile in /Users/xxx/.gvm/gos/go1.18/src/runtime/pprof/pprof.go
         0          1 (flat, cum) 0.033% of Total

同样通过top和list大法,找出协程分配较多的地方。

pprof工具的可视化界面
使用方法
# 方法一
go tool pprof -http=:8091 ${source}

# 方法二,前提是要安装 pprof ,具体方法请查看“ pprof 工具准备”章节
pprof -http=:8091 ${source}

# 方法三,在交互式界面上执行 web 指令
go tool pprof ${source}
(pprof) web
cpu分析

执行命令

go tool pprof -http=:8091 http://localhost:8090/debug/pprof/profile?seconds=15

可视化Graph
cpu_profile_graph_view
框越大线越粗,代表该代码处占用的时间越长。

可视化Top
cpu_profile_top_view

可视化火焰图
cpu_profile_flame_graph

火焰图(Flame Graph)是由 Linux 性能优化大师 Brendan Gregg 发明的,它以一个全局的视野来看待时间分布,并且列出所有可能导致性能瓶颈的调用栈。
火焰图有以下特征:

  • 每一列代表一个调用栈,每一个格子代表一个函数。
  • 纵轴展示了栈的深度,正立火焰图按照调用关系从下到上排列(倒立火焰图方向刚好相反)。
  • 横轴部分,火焰图将采集的多个调用栈信息,通过按字母横向排序的方式将众多信息聚合在一起。需要注意的是它并不代表时间的先后。横轴格子的宽度代表其在采样中出现频率,所以一个格子的宽度越大,说明它是瓶颈原因的可能性就越大。
  • 火焰图格子的颜色是随机的暖色调,方便区分各个调用信息。

可视化Peek
cpu_profile_peek_view

可视化Source
cpu_profile_source_view

内存分析
go tool pprof -http=:8091 http://localhost:8090/debug/pprof/heap

可视化Graph
heap_profile_graph_view
其他图表请参考 cpu 分析部分,这里不再一一列举。

协程分析

执行命令

go tool pprof -http=:8091 http://localhost:8090/debug/pprof/goroutine

可视化Graph
goroutine_profile_graph_view
其他图表请参考 cpu 分析部分,这里不再一一列举。

使用runtime/pprof生成profile文件调试

之前的章节使用 net/http/pprof 提供的 HTTP 接口进行调试分析,如果是非 HTTP 的程序或者因为数据安全问题,我们不想暴露 HTTP 接口,那么我们该如何分析调试?答案是在代码中引入 runtime/pprof 包,示例代码如下:

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")

func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal("could not create CPU profile: ", err)
        }
        defer f.Close() // error handling omitted for example
        // 启动CPU Profile
        if err := pprof.StartCPUProfile(f); err != nil {
            log.Fatal("could not start CPU profile: ", err)
        }
        // 停止CPU Profile
        defer pprof.StopCPUProfile()
    }

    // ... rest of the program ...

    if *memprofile != "" {
        f, err := os.Create(*memprofile)
        if err != nil {
            log.Fatal("could not create memory profile: ", err)
        }
        defer f.Close() // error handling omitted for example
        runtime.GC() // get up-to-date statistics
        // 生成当前内存统计的 Heap Profile
        if err := pprof.WriteHeapProfile(f); err != nil {
            log.Fatal("could not write memory profile: ", err)
        }
    }
}

完整代码请参考 demo 工程的 pprof/internal/xxprof/xxprof.go ,通过上面的代码生成相关的 profile 文件之后,使用 pprof 工具即可分析调试。

另外,为了便于使用,我们可以结合 signal 来使用,示例代码

package xxsignal

import (
        "log"
        "os"
        "os/signal"
        "syscall"

        "github.com/rukesun/go-learning/examples/pprof/internal/xxprof"
)

func init() {
        go func() {
                var profiler *xxprof.Profile

                signals := make(chan os.Signal, 1)
                signal.Notify(signals, syscall.SIGUSR1, syscall.SIGUSR2)

                for {
                        v := <-signals
                        log.Printf("Got signal:", v)
                        switch v {
                        case syscall.SIGUSR1:
                        case syscall.SIGUSR2:
                                if profiler == nil {
                                        profiler = xxprof.NewProfile()
                                        profiler.Start()
                                } else {
                                        profiler.Stop()
                                        profiler = nil
                                }
                        default:
                                log.Printf("Got unregistered signal:", v)
                        }
                }
        }()
}

在需要使用的程序 import xxsignal ,然后通过kill发送相关的信号即可。注意,使用 signal 时,需要使用 go build 编译成二进制可执行文件,通过 go run 的方式则不能捕获相关的信号。

kill -SIGUSR2 ${pid}

参考资料

  1. Debugging in Visual Studio Code, https://code.visualstudio.com/docs/editor/debugging
  2. Running | Goland, https://www.jetbrains.com/help/go/running-applications.html
  3. Debugging | Goland, https://www.jetbrains.com/help/go/debugging-code.html
  4. PProf README, https://github.com/google/pprof/blob/master/doc/README.md
  5. Profiling a Go program, https://pkg.go.dev/runtime/pprof@go1.18#hdr-Profiling_a_Go_program
  6. Go tool pprof, https://github.com/eddycjy/blog/blob/master/content/posts/go/tools/2018-09-15-go-tool-pprof.md
  7. Go pprof practice, https://blog.wolfogre.com/posts/go-ppof-practice/
  8. Go Zero profile, https://github.com/zeromicro/go-zero/blob/master/core/proc/profile.go

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值