1、最常用的调试 golang 的 bug 以及性能问题的实践方法?

    这个方法不算新颖,但是确很实用。 是Unix/Linux内置多命令,使用时一般不用传过多参数,直接跟上需要调试多程序即可。

    上面是使用time对 go run test2.go对执行程序坐了性能分析,得到3个指标。

    • real:从程序开始到结束,实际度过的时间;
    • user:程序在用户态度过的时间;
    • sys:程序在内核态度过的时间。

    一般情况下 real >= user + sys,因为系统还有其它进程(切换其他进程中间对于本进程会有空白期)。

    (2) /usr/bin/time指令

    这个指令比内置的time更加详细一些,使用的时候需要用绝对路径,而且要加上参数-v

    1. $ /usr/bin/time -v go run test2.go
    2. Command being timed: "go run test2.go"
    3. User time (seconds): 0.12
    4. System time (seconds): 0.06
    5. Percent of CPU this job got: 115%
    6. Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.16
    7. Average shared text size (kbytes): 0
    8. Average unshared data size (kbytes): 0
    9. Average stack size (kbytes): 0
    10. Average total size (kbytes): 0
    11. Maximum resident set size (kbytes): 41172
    12. Average resident set size (kbytes): 0
    13. Major (requiring I/O) page faults: 1
    14. Minor (reclaiming a frame) page faults: 15880
    15. Voluntary context switches: 897
    16. Involuntary context switches: 183
    17. Swaps: 0
    18. File system inputs: 256
    19. File system outputs: 2664
    20. Socket messages sent: 0
    21. Socket messages received: 0
    22. Signals delivered: 0
    23. Page size (bytes): 4096
    24. Exit status: 0

    可以看到这里的功能要强大多了,除了之前的信息外,还包括了:

    • CPU占用率;
    • 内存使用情况;
    • Page Fault 情况;
    • 进程切换情况;
    • 文件系统IO;
    • Socket 使用情况;
    • ……

    我们先写一段demo例子代码

    1. package main
    2. import (
    3. "log"
    4. "runtime"
    5. "time"
    6. )
    7. func test() {
    8. //slice 会动态扩容,用slice来做堆内存申请
    9. container := make([]int, 8)
    10. log.Println(" ===> loop begin.")
    11. for i := 0; i < 32*1000*1000; i++ {
    12. container = append(container, i)
    13. }
    14. log.Println(" ===> loop end.")
    15. }
    16. func main() {
    17. log.Println("Start.")
    18. test()
    19. log.Println("force gc.")
    20. runtime.GC() //强制调用gc回收
    21. log.Println("Done.")
    22. time.Sleep(3600 * time.Second) //睡眠,保持程序不退出
    23. }

    编译

    1. $go build -o snippet_mem && ./snippet_mem

    然后在./snippet_mem进程没有执行完,我们再开一个窗口,通过top命令查看进程的内存占用情况

    1. $top -p $(pidof snippet_mem)

    得到结果如下:

    我们看出来,没有退出的snippet_mem进程有约830m的内存被占用。

    直观上来说,这个程序在test()函数执行完后,切片contaner的内存应该被释放,不应该占用830M那么大。

    下面让我们使用GODEBUG来分析程序的内存使用情况。


    (2) GODEBUG与gctrace

    用法

    执行snippet_mem程序之前添加环境变量GODEBUG='gctrace=1'来跟踪打印垃圾回收器信息

    1. $ GODEBUG='gctrace=1' ./snippet_mem

    设置gctrace=1会使得垃圾回收器在每次回收时汇总所回收内存的大小以及耗时, 并将这些内容汇总成单行内容打印到标准错误输出中。

    格式

    1. gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P

    含义

    1. gc # GC次数的编号,每次GC时递增
    2. @#s 距离程序开始执行时的时间
    3. #% GC占用的执行时间百分比
    4. #+...+# GC使用的时间
    5. #->#-># MB GC开始,结束,以及当前活跃堆内存的大小,单位M
    6. # MB goal 全局堆内存大小
    7. # P 使用processor的数量

    如果每条信息最后,以(forced)结尾,那么该信息是由runtime.GC()调用触发

    我们来选择其中一行来解释一下:

    1. gc 17 @0.149s 1%: 0.004+36+0.003 ms clock, 0.009+0/0.051/36+0.006 ms cpu, 181->181->101 MB, 182 MB goal, 2 P

    该条信息含义如下:

    • gc 17: Gc 调试编号为17
    • @0.149s:此时程序已经执行了0.149s
    • 1%: 0.149s中其中gc模块占用了1%的时间
    • 0.004+36+0.003 ms clock: 垃圾回收的时间,分别为STW(stop-the-world)清扫的时间+并发标记和扫描的时间+STW标记的时间
    • 0.009+0/0.051/36+0.006 ms cpu: 垃圾回收占用cpu时间
    • 181->181->101 MB: GC开始前堆内存181M, GC结束后堆内存181M,当前活跃的堆内存101M
    • 182 MB goal: 全局堆内存大小
    • 2 P: 本次GC使用了2个P(调度器中的Processer)

    了解了GC的调试信息读法后,接下来我们来分析一下本次GC的结果。

    我们还是执行GODEBUG调试

    1. $ GODEBUG='gctrace=1' ./snippet_mem

    结果如下

    分析

    ​ 先看在test()函数执行完后立即打印的gc 21那行的信息。444->444->0 MB, 888 MB goal表示垃圾回收器已经把444M的内存标记为非活跃的内存。

    再看下一个记录gc 220->0->0 MB, 4 MB goal表示垃圾回收器中的全局堆内存大小由888M下降为4M

    结论

    1、在test()函数执行完后,demo程序中的切片容器所申请的堆空间都被垃圾回收器回收了。

    接下来我么换另一种方式查看内存的方式 利用 runtime库里的ReadMemStats()方法

    demo2.go

    1. package main
    2. import (
    3. "log"
    4. "runtime"
    5. "time"
    6. )
    7. func readMemStats() {
    8. var ms runtime.MemStats
    9. runtime.ReadMemStats(&ms)
    10. log.Printf(" ===> Alloc:%d(bytes) HeapIdle:%d(bytes) HeapReleased:%d(bytes)", ms.Alloc, ms.HeapIdle, ms.HeapReleased)
    11. }
    12. func test() {
    13. //slice 会动态扩容,用slice来做堆内存申请
    14. container := make([]int, 8)
    15. log.Println(" ===> loop begin.")
    16. for i := 0; i < 32*1000*1000; i++ {
    17. container = append(container, i)
    18. if ( i == 16*1000*1000) {
    19. readMemStats()
    20. }
    21. }
    22. }
    23. log.Println(" ===> [Start].")
    24. readMemStats()
    25. test()
    26. readMemStats()
    27. log.Println(" ===> [force gc].")
    28. runtime.GC() //强制调用gc回收
    29. log.Println(" ===> [Done].")
    30. readMemStats()
    31. go func() {
    32. for {
    33. readMemStats()
    34. time.Sleep(10 * time.Second)
    35. }
    36. }()
    37. time.Sleep(3600 * time.Second) //睡眠,保持程序不退出
    38. }

    这里我们, 封装了一个函数readMemStats(),这里面主要是调用runtime中的ReadMemStats()方法获得内存信息,然后通过log打印出来。

    我们执行一下代码并运行

    1. $ go run demo2.go
    2. 2020/03/02 18:21:17 ===> [Start].
    3. 2020/03/02 18:21:17 ===> Alloc:71280(bytes) HeapIdle:66633728(bytes) HeapReleased:66600960(bytes)
    4. 2020/03/02 18:21:17 ===> loop begin.
    5. 2020/03/02 18:21:18 ===> Alloc:132535744(bytes) HeapIdle:336756736(bytes) HeapReleased:155721728(bytes)
    6. 2020/03/02 18:21:38 ===> loop end.
    7. 2020/03/02 18:21:38 ===> Alloc:598300600(bytes) HeapIdle:609181696(bytes) HeapReleased:434323456(bytes)
    8. 2020/03/02 18:21:38 ===> [force gc].
    9. 2020/03/02 18:21:38 ===> [Done].
    10. 2020/03/02 18:21:38 ===> Alloc:55840(bytes) HeapIdle:1207427072(bytes) HeapReleased:434266112(bytes)
    11. 2020/03/02 18:21:38 ===> Alloc:56656(bytes) HeapIdle:1207394304(bytes) HeapReleased:434266112(bytes)
    12. 2020/03/02 18:21:48 ===> Alloc:56912(bytes) HeapIdle:1207394304(bytes) HeapReleased:1206493184(bytes)
    13. 2020/03/02 18:21:58 ===> Alloc:57488(bytes) HeapIdle:1207394304(bytes) HeapReleased:1206493184(bytes)
    14. 2020/03/02 18:22:08 ===> Alloc:57616(bytes) HeapIdle:1207394304(bytes) HeapReleased:1206493184(bytes)
    15. c2020/03/02 18:22:18 ===> Alloc:57744(bytes) HeapIdle:1207394304(bytes) HeapReleased:1206493184(by

    ​ 可以看到,打印[Done].之后那条trace信息,Alloc已经下降,即内存已被垃圾回收器回收。在2020/03/02 18:21:382020/03/02 18:21:48的两条trace信息中,HeapReleased开始上升,即垃圾回收器把内存归还给系统。

    另外,MemStats还可以获取其它哪些信息以及字段的含义可以参见官方文档:

    (4)pprof工具

    pprof工具支持网页上查看内存的使用情况,需要在代码中添加一个协程即可。

    1. import(
    2. "net/http"
    3. _ "net/http/pprof"
    4. )
    5. go func() {
    6. log.Println(http.ListenAndServe("0.0.0.0:10000", nil))
    7. }()

    具体添加的完整代码如下:

    demo3.go

    1. package main
    2. import (
    3. "log"
    4. "runtime"
    5. "time"
    6. "net/http"
    7. _ "net/http/pprof"
    8. )
    9. func readMemStats() {
    10. var ms runtime.MemStats
    11. runtime.ReadMemStats(&ms)
    12. log.Printf(" ===> Alloc:%d(bytes) HeapIdle:%d(bytes) HeapReleased:%d(bytes)", ms.Alloc, ms.HeapIdle, ms.HeapReleased)
    13. }
    14. func test() {
    15. //slice 会动态扩容,用slice来做堆内存申请
    16. container := make([]int, 8)
    17. log.Println(" ===> loop begin.")
    18. for i := 0; i < 32*1000*1000; i++ {
    19. container = append(container, i)
    20. if ( i == 16*1000*1000) {
    21. readMemStats()
    22. }
    23. }
    24. log.Println(" ===> loop end.")
    25. }
    26. func main() {
    27. //启动pprof
    28. go func() {
    29. log.Println(http.ListenAndServe("0.0.0.0:10000", nil))
    30. }()
    31. log.Println(" ===> [Start].")
    32. readMemStats()
    33. test()
    34. readMemStats()
    35. log.Println(" ===> [force gc].")
    36. runtime.GC() //强制调用gc回收
    37. log.Println(" ===> [Done].")
    38. readMemStats()
    39. go func() {
    40. for {
    41. readMemStats()
    42. time.Sleep(10 * time.Second)
    43. }
    44. }()
    45. time.Sleep(3600 * time.Second) //睡眠,保持程序不退出
    46. }

    我们正常运行程序,然后同时打开浏览器,

    输入地址:

    浏览器的内容其中有一部分如下,记录了目前的内存情况

    1. # ...
    2. # runtime.MemStats
    3. # Alloc = 228248
    4. # TotalAlloc = 1293696976
    5. # Sys = 834967896
    6. # Lookups = 0
    7. # Mallocs = 2018
    8. # Frees = 671
    9. # HeapAlloc = 228248
    10. # HeapSys = 804913152
    11. # HeapIdle = 804102144
    12. # HeapInuse = 811008
    13. # HeapReleased = 108552192
    14. # HeapObjects = 1347
    15. # Stack = 360448 / 360448
    16. # MSpan = 28288 / 32768
    17. # MCache = 3472 / 16384
    18. # BuckHashSys = 1449617
    19. # GCSys = 27418976
    20. # LastGC = 1583203571137891390
    21. # ...
    • 性能分析必须在一个

      可重复的、稳定的环境中来进行。

      • 机器必须闲置
        • 不要在共享硬件上进行性能分析;
        • 不要在性能分析期间,在同一个机器上去浏览网页
      • 注意省电模式和过热保护,如果突然进入这些模式,会导致分析数据严重不准确
      • 不要使用虚拟机、共享的云主机,太多干扰因素,分析数据会很不一致;
      • 不要在 macOS 10.11 及以前的版本运行性能分析,有 bug,之后的版本修复了。

    如果承受得起,购买专用的性能测试分析的硬件设备,上架。

    • 关闭电源管理、过热管理;
    • 绝不要升级,以保证测试的一致性,以及具有可比性。

    如果没有这样的环境,那就一定要在多个环境中,执行多次,以取得可参考的、具有相对一致性的测试结果。

    (2) CPU性能分析

    我们来用下面的代码进行测试

    1. package main
    2. import (
    3. "bytes"
    4. "math/rand"
    5. "time"
    6. "log"
    7. "net/http"
    8. _ "net/http/pprof"
    9. )
    10. func test() {
    11. log.Println(" ===> loop begin.")
    12. for i := 0; i < 1000; i++ {
    13. log.Println(genSomeBytes())
    14. }
    15. log.Println(" ===> loop end.")
    16. }
    17. //生成一个随机字符串
    18. func genSomeBytes() *bytes.Buffer {
    19. var buff bytes.Buffer
    20. for i := 1; i < 20000; i++ {
    21. buff.Write([]byte{'0' + byte(rand.Intn(10))})
    22. }
    23. return &buff
    24. }
    25. func main() {
    26. go func() {
    27. for {
    28. test()
    29. time.Sleep(time.Second * 1)
    30. }
    31. }()
    32. //启动pprof
    33. http.ListenAndServe("0.0.0.0:10000", nil)
    34. }

    这里面还是启动了pprof的坚挺,有关pprof启动的代码如下

    1. import (
    2. "net/http"
    3. _ "net/http/pprof"
    4. )
    5. func main() {
    6. //...
    7. //...
    8. //启动pprof
    9. http.ListenAndServe("0.0.0.0:10000", nil)
    10. }

    main()里的流程很简单,启动一个goroutine去无限循环调用test()方法,休眠1s.

    test()的流程是生成1000个20000个字符的随机字符串.并且打印.

    我们将上面的代码编译成可执行的二进制文件 demo4(记住这个名字,稍后我们能用到)

    1. $ go build demo4.go

    接下来我们启动程序,程序会无限循环的打印字符串.

    接下来我们通过几种方式来查看进程的cpu性能情况.

    A. Web界面查看

    浏览器访问http://127.0.0.1:10000/debug/pprof/

    我们会看到如下画面

    1、最常用的调试 golang 的 bug 以及性能问题的实践方法? - 图2

    有关profile下面的英文解释大致如下:

    “CPU配置文件。您可以在秒GET参数中指定持续时间。获取概要文件后,请使用go tool pprof命令调查概要文件。”

    所以我们要是想得到cpu性能,就是要获取到当前进程的profile文件,这个文件默认是30s生成一个,所以你的程序要至少运行30s以上(这个参数也可以修改,稍后我们介绍)

    我们可以直接点击网页的profile,浏览器会给我们下载一个profile文件. 记住这个文件的路径, 可以拷贝到与demo4所在的同一文件夹下.

    B. 使用pprof工具查看

    pprof 的格式如下

    1. go tool pprof [binary] [profile]

    binary: 必须指向生成这个性能分析数据的那个二进制可执行文件;

    profile: 必须是该二进制可执行文件所生成的性能分析数据文件。

    binaryprofile 必须严格匹配

    我们来查看一下:

    help可以查看一些指令,我么可以通过top来查看cpu的性能情况.

    1. (pprof) top
    2. Showing nodes accounting for 5090ms, 81.18% of 6270ms total
    3. Dropped 80 nodes (cum <= 31.35ms)
    4. Showing top 10 nodes out of 60
    5. flat flat% sum% cum cum%
    6. 1060ms 16.91% 16.91% 2170ms 34.61% math/rand.(*lockedSource).Int63
    7. 850ms 13.56% 30.46% 850ms 13.56% sync.(*Mutex).Unlock (inline)
    8. 710ms 11.32% 41.79% 2950ms 47.05% math/rand.(*Rand).Int31n
    9. 570ms 9.09% 50.88% 990ms 15.79% bytes.(*Buffer).Write
    10. 530ms 8.45% 59.33% 540ms 8.61% syscall.Syscall
    11. 370ms 5.90% 65.23% 370ms 5.90% runtime.procyield
    12. 270ms 4.31% 69.54% 4490ms 71.61% main.genSomeBytes
    13. 250ms 3.99% 73.52% 3200ms 51.04% math/rand.(*Rand).Intn
    14. 250ms 3.99% 77.51% 250ms 3.99% runtime.memmove
    15. 230ms 3.67% 81.18% 690ms 11.00% runtime.suspendG
    16. (pprof)

    这里面有几列数据,需要说明一下.

    • flat:当前函数占用CPU的耗时
    • flat::当前函数占用CPU的耗时百分比
    • sun%:函数占用CPU的耗时累计百分比
    • cum:当前函数加上调用当前函数的函数占用CPU的总耗时
    • cum%:当前函数加上调用当前函数的函数占用CPU的总耗时百分比
    • 最后一列:函数名称

    通过结果我们可以看出, 该程序的大部分cpu性能消耗在 main.getSoneBytes()方法中,其中math/rand取随机数消耗比较大.

    C. 通过go tool pprof得到profile文件

    我们上面的profile文件是通过web浏览器下载的,这个profile的经过时间是30s的,默认值我们在浏览器上修改不了,如果你想得到时间更长的cpu利用率,可以通过go tool pprof指令与程序交互来获取到

    首先,我们先启动程序

    1. $ ./demo4

    然后再打开一个终端

    1. go tool pprof http://localhost:10000/debug/pprof/profile?seconds=60

    这里制定了生成profile文件的时间间隔60s

    等待60s之后, 终端就会有结果出来,我们继续使用top来查看.

    1. $ go tool pprof http://localhost:10000/debug/pprof/profile?seconds=60
    2. Fetching profile over HTTP from http://localhost:10000/debug/pprof/profile?seconds=60
    3. Saved profile in /home/itheima/pprof/pprof.demo4.samples.cpu.005.pb.gz
    4. File: demo4
    5. Type: cpu
    6. Time: Mar 3, 2020 at 11:59pm (CST)
    7. Duration: 1mins, Total samples = 12.13s (20.22%)
    8. Entering interactive mode (type "help" for commands, "o" for options)
    9. (pprof) top
    10. Showing nodes accounting for 9940ms, 81.95% of 12130ms total
    11. Dropped 110 nodes (cum <= 60.65ms)
    12. Showing top 10 nodes out of 56
    13. flat flat% sum% cum cum%
    14. 2350ms 19.37% 19.37% 4690ms 38.66% math/rand.(*lockedSource).Int63
    15. 1770ms 14.59% 33.97% 1770ms 14.59% sync.(*Mutex).Unlock (inline)
    16. 1290ms 10.63% 44.60% 6040ms 49.79% math/rand.(*Rand).Int31n
    17. 1110ms 9.15% 53.75% 1130ms 9.32% syscall.Syscall
    18. 810ms 6.68% 60.43% 1860ms 15.33% bytes.(*Buffer).Write
    19. 620ms 5.11% 65.54% 6660ms 54.91% math/rand.(*Rand).Intn
    20. 570ms 4.70% 70.24% 570ms 4.70% runtime.procyield
    21. 500ms 4.12% 74.36% 9170ms 75.60% main.genSomeBytes
    22. 480ms 3.96% 78.32% 480ms 3.96% runtime.memmove
    23. 440ms 3.63% 81.95% 440ms 3.63% math/rand.(*rngSource).Uint64
    24. (pprof)

    依然会得到cpu性能的结果, 我们发现这次的结果与上次30s的结果百分比类似.

    D.可视化查看

    我们还是通过

    1. $ go tool pprof ./demo4 profile

    进入profile文件查看,然后我们输入web指令.

    1. $ go tool pprof ./demo4 profileFile: demo4
    2. Type: cpu
    3. Time: Mar 3, 2020 at 11:18pm (CST)
    4. Duration: 30.13s, Total samples = 6.27s (20.81%)
    5. Entering interactive mode (type "help" for commands, "o" for options)
    6. (pprof) web

    这里如果报找不到graphviz工具,需要安装一下

    Ubuntu安装

    1. $sudo apt-get install graphviz

    Mac安装

    1. brew install graphviz

    windows安装

    下载https://graphviz.gitlab.io/_pages/Download/Download_windows.html

    1. graphviz安装目录下的bin文件夹添加到Path环境变量中。 在终端输入dot -version查看是否安装成功。

    然后我们得到一个svg的可视化文件在路径下

    这样我们就能比较清晰的看到函数之间的调用关系,方块越大的表示cpu的占用越大.