本文主要讲解如何调试golang程序,内容包括 GODEBUG 和 性能分析(pprof) 两部分。
案例代码在
要使测试某个用例的时候,在main.go中取消相应的注释即可。
GODEBUG
GODEBUG 变量是控制 runtime调试变量的变量。调试的变量有点多,这里就挑几个讲重要的讲解。详情可以移步
gctrace
设置 gctrace=1 会使 垃圾回收器 在每次GC时,打印一行设涉及到GC的信息,同时汇总收集的内存量和暂停的时间。
咱们结合用例代码的输出来说说每列数据的意思。输出如下:
随便挑一行来收说
这里重点说一下 wall-clock 和 cpu 时间的区别
wall-clock time:是指程序开始执行到完成所经历的实际时间(墙上挂钟时间)
cpu time:是指程序使用 CPU 的时间
他们存在以下关系:
这一行的内容是在 "slice alloc end"之前。 之后我们手动执行 "runtime.GC()", gc 23 这一行 后面 “(forced)” 就代表了 被 runtime.GC() 强制执行。然后我们看到当前活跃内存的大小已经为0。但是发现全局堆大小是 888 M,即当前进程还占用了888M进程,这些都是空闲内存,它会慢慢被系统回收。
inittrace
inittrace=1
像上面一样,咱们直接看用例输出(这里只需要把 init_package1、init_package2引入到main.go中即可)
挑一行来解释
从输出,我们还可以看到 依赖包里面依赖包的内容。打印顺序同依赖顺序
madvdontneed
madvdontneed=1(madvdontneed=0, MADV_FREE)
schedtrace
schedtrace=X
看看用例输出
老规矩,随便找一行来说明
scheddetail
schedtrace=X,scheddetail=1
用例同schedtrace,只需要多加一个GODEBUG参数即可。输出如下
这里我们能详细的看到 一个时间段的 GMP相关信息。分析上面详情内容之前,咱们先了解一下G包含哪些状态:
以及 P (processor)包含的状态:
然后我们再解释输出的内容
性能分析(pprof)
"runtime/trace"_ "net/http/pprof"
我们直接使用 go 自带的性能分析工具即可, 而且最火焰图早已经已经集成进了 go tool pprof。
用例直接在 main.go 里能很清晰的看到,相应的取消注释即可,同时记得要放开下面的
cpu分析
go run main.gogo tool pprof -http=:端口 "http://127.0.0.1:8989/debug/pprof/profile?seconds=60"
60s 后,生成了报告文件 XXX.pb.gz,同时自动打开一个web页面展示分析结果。
我们直接在View里选中 Flame Graph(火焰图) 展示即可。读者可以自己测试,这里就不截图展示了。可以很清晰的看出每个方法消耗的cpu time,以及子方法的时间占比,从用例中我们发现wasteCPU方法占用了大量时间。
heap分析
go run main.gogo tool pprof -http=:端口 "http://127.0.0.1:8989/debug/pprof/heap?seconds=60"
SAMPLE标签
协程栈分析
race
这里没有用例,因为写出一个有数据竞争的协程例子很容易。
go build -raceWARNING: DATA RACE
协程泄漏
/debug/pprof/goroutine
我们发现大量的协程调用栈最终都会指向 runtime.gopark 函数。
_Gwaiting
其实我们通过 直接访问 127.0.0.1:8989/debug/pprof 发现goroutine的数量不断增加,也可以初步判断协程泄漏了
mutex阻塞分析
即我们分析互斥锁的竞争情况。
/debug/pprof/mutex?seconds=30
在报告中我们会发现互斥锁带来的协程休眠时间是30s 左右, 所以我们可以断定 cpu利用率不高,大量的时间浪费在了这里,需要调整锁临界区(lock与Unlock之间的代码区域)的代码。
trace事件追踪
基于上文的各种分析指标,其实已经能覆盖大部分场景了。
但那些指标并没有提供 Goroutine 整个周期内(生成报告的时间内)发生的时间,例如 Goroutine 何时创建、执行了多少时间、调度到了哪个processor上执行、GC如何影响协程的 等等信息。
go1.5在之后的推出的, 它提供了指定时间内程序发生的事件的完整信息,包括:
- 协程的创建、开始和结束
- 协程的阻塞 -- 系统调用、通道、锁
- 网络I/O相关事件
- 系统调用事件
- 垃圾回收相关事件
用例在这
这里有点不同的是,为了方便测试使用的是"runtime/trace"依赖包。也可以使用接口的方式再配合压测,也能得出报告。执行方式如下:
这里工具也不再是 pprof, 而是trace
然后我们就得出报告界面:
从简单到复杂,一点一点来看(只挑几个重点讲)
咱们先看最简单的,点击 "Scheduler latency profile":
这个图咱们不陌生,跟 cpu 分析出来的结果展示一样,就是一个方法调用链加时间的图。
然后再点击 "Goroutine analysis":
可以看到具体哪些协程。旁边的 N 表示数量。
点击一个协程 "go_debug/example/pprof.TestTrace·dwrap·2" 可以看到其编号,以及在整个运行过程中,各类时间的占比
最后看 点击 “View trace” 查看程序整个生命周期中各个 Goroutine 发生的事情,比如什么时候发生了GC、协程被调度到了哪个 processor:
我在图上做了几个红色的编号:
我们点击第一个绿色的协程执行片段(go_debug/example/pprof.TestTrace dwrap 2),会看到下面的信息
总结
如果把我们的golang 进程当作一个黑盒(不了解代码),通过上文我们可以从宏观(GODEBUG)、微观(pprof + trace) 两个维度得出来的指标信息,来分析出该进程存在哪些问题。
不仅如此,从这么多年的从业经验来看,在测试阶段工程师有性能分析的好习惯(比如本身就存在 内存泄漏、协程泄漏),可以有效避免线上一些奇怪的事故,功能测试是很难测出这类问题的。