• 爱上开源之golang入门至实战第三章goroutine分析


     爱上开源之golang入门至实战第三章 - goroutine分析

    Goroutine

    Pprof中的Goroutine是对当前时间点的goroutine(协程)数据的采样,我们经常使用pprof对可能发送goroutine(协程)泄漏的可能点进行分析;goroutine(协程)泄漏是goroutine启动之后没有退出导致goroutine的数量不会减少,或者是在实际应用中goroutine占用了很长时间才退出导致在一段时间内goroutine的数量急剧上升; 虽然goroutine(协程)相对于线程的开销来说更加的轻量级,但是对于一个高并发的对性能要求比较高的系统,一样会由于协程的持续无谓的开销而导致整体性能的降低;甚至更严重的由于协程不断的增加,最后由于资源不够充足而无法进行分配,导致整个系统宕机。

    我们还是先一起来看看;pprof提供出来的goroutine是怎样的采样数据; 如下图所示:

    如果了解以上的采样数据和数据格式表示的什么信息; 我们可以查看runtime/pprof/pprof.go的源代码

    runtime/pprof/pprof.go源代码Line 709是Goroutine的pprof采样数据的输出入口

    1. func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]runtime.StackRecord, []unsafe.Pointer) (int, bool)) error {
    2. // Find out how many records there are (fetch(nil)),
    3. // allocate that many records, and get the data.
    4. // There's a race—more records might be added between
    5. // the two calls—so allocate a few extra records for safety
    6. // and also try again if we're very unlucky.
    7. // The loop should only execute one iteration in the common case.
    8. var p []runtime.StackRecord
    9. var labels []unsafe.Pointer
    10. n, ok := fetch(nil, nil)
    11. for {
    12. // Allocate room for a slightly bigger profile,
    13. // in case a few more entries have been added
    14. // since the call to ThreadProfile.
    15. p = make([]runtime.StackRecord, n+10)
    16. labels = make([]unsafe.Pointer, n+10)
    17. n, ok = fetch(p, labels)
    18. if ok {
    19. p = p[0:n]
    20. break
    21. }
    22. // Profile grew; try again.
    23. }
    24. return printCountProfile(w, debug, name, &runtimeProfile{p, labels})
    25. }

    printCountProfile函数Line 435 可以找到goroutine的具体输出内容; 入口函数如下

    1. if debug > 0 {
    2. // Print debug profile in legacy format
    3. tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
    4. fmt.Fprintf(tw, "%s profile: total %d\n", name, p.Len())
    5. for _, k := range keys {
    6. fmt.Fprintf(tw, "%d %s\n", count[k], k)
    7. printStackRecord(tw, p.Stack(index[k]), false)
    8. }
    9. return tw.Flush()
    10. }

    技巧

    选择一个好的IDE对于语言的学习非常的有作用;golang的IDE,笔者首选intellij公司出品的Idea;Idea功能强大;体验感非常的强;用idea来研究golang的源代码非常的高效

    下图为Idea里的源代码片段

    技巧

    在Idea里,输入ctrl+Shift+R就可以出现快捷搜索框,在搜索框里输入pprof.go就可以快速把pprof.go源文件定位出来;在Idea的左下窗口,structure window可以显示出该源文件里定义的Type, 结构体,函数。变量等等;

    在源代码窗口里, 点击ctrl同时鼠标移动到函数名上面, 就可以直接看到该函数的具体信息;如果需要进一步查看该函数的实现内容,点击函数名,就可以直接跳转到函数的方法体;通过idea的熟练使用;在查看和分析golang的源代码的,完全可以做到事半功倍

    在golang的源代码里,可以看到很多文档或者网上资料都没有见到过的东西;毕竟相对于java或者其他语言;golang还相对年轻;并且由于golang相对于java的优点,更让golang适合去做一些云原生生态里的底仓服务;本身对开发者要求高;涉众没有java那么广泛,生态圈和社区都没有特别全面;一些底仓的东西;有时候我们必须学会去翻源代码已确定实现的细节

    下面来看看goroutine pprof中使用

    goroutine: Stack traces of all current goroutines

    所以首先要明确的就是; goroutine pprof是程序栈里当前的所有协程; 这里强调的当前的;即这个也是一个快照量;反映的是当前采集时间点上的协程状况

    Goroutine的手工埋点

    在很多的文档和网上资料里, 往往只介绍了如何通过net/http/pprof的方式进行web服务的pprof埋点;关于手工在程序里进行埋点的方式也只介绍了CPU Profiling和Heap的埋点方式,其他的埋点方式网上和文档都没有介绍,在pprof包里可以export的方法也只有StartCPUProfileWriteHeapProfile两个,其他都没有提供export的方法,笔者通过pprof的源代码找到了其他的埋点方法,goroutine的埋点方法如下:

    1. routineProfile := pprof.Lookup("goroutine")
    2. if err := routineProfile.WriteTo(f2, 0); err != nil {
    3. panic("could not start goroutine-1 profile: ")
    4. }

    通过在这段代码,就可以把当前程序栈中的协程采样数据记录到指定的文件里;然后通过go tool pprof goroutine-prof-1 命令;就可以进行goroutine的pprof分析了。

    下面通过一个实例,来演示一下goroutine pprof分析; 演示程序在主协程里启动多个协程,协程在收到指定的通道信号后,退出协程;没有收到通道的退出信号前;协程处于阻塞状态;然后在主协程里,进行goroutine采样; 采样结束后;往指定的通道发送退出信号;子协程全部退出后,在主协程再次进行goroutine采样;

    代码如下

    1. func TestGoroutinePprof(t *testing.T) {
    2. var profileEnd = "goroutine-prof-1"  // 指定保存的文件路径,当前目录的goroutine-prof-1文件
    3. var profileEnd2 = "goroutine-prof-2" // 指定保存的文件路径,当前目录的goroutine-prof-2文件
    4. f2, err := os.Create(profileEnd)
    5. if err != nil {
    6. panic(err)
    7. }
    8. defer f2.Close()
    9. f3, err := os.Create(profileEnd2)
    10. if err != nil {
    11. panic(err)
    12. }
    13. defer f3.Close()
    14. var goroutine = 100
    15. var wg sync.WaitGroup
    16. wg.Add(goroutine)
    17. var exit = make(chan bool)
    18. for idx := 1; idx <= goroutine; idx++ {
    19. func(i int) {
    20. go func() {
    21. <-exit
    22. fmt.Printf("No %d exit\n", i)
    23. wg.Done()
    24. }()
    25. }(idx)
    26. }
    27. time.Sleep(500 * time.Millisecond)
    28. //var profileEnd3 = "heap-prof-3" // 指定保存的文件路径,当前目录的test-prof文件
    29. routineProfile := pprof.Lookup("goroutine")
    30. if err := routineProfile.WriteTo(f2, 0); err != nil {
    31. panic("could not start goroutine-1 profile: ")
    32. }
    33. close(exit)
    34. wg.Wait()
    35. if err := routineProfile.WriteTo(f3, 0); err != nil {
    36. panic("could not start goroutine-2 profile: ")
    37. }
    38. fmt.Printf("sum=%d\n", goroutine)
    39. }

    技巧

    golang具有多并发编程的良好基因, 通过上面的go func(){}的方式,就可以非常方便的启动一个比线程轻量很多的routine; 同时通过chan的实现; 进行不同routine里的消息通信;通过WaitGroup的方式实现主子协程同步的操作

    执行上面的代码,产生goroutine-prof-1goroutine-prof-2两个goroutine的采样数据; goroutine-prof-1对应的是启动了所欲的子协程以后的goroutine的采样数据,goroutine-prof-2对应的是所有子协程收到退出信号后;子协程全部都确保退出后以后的goroutine采样数据。

    通过pprof对采样数据进行分析

    1. PS E:\WORK\PROJECT\git\go\go-in-practice\code\charpter-01> go tool pprof goroutine-prof-1
    2. warning: GOPATH set to GOROOT (E:\WORK\SOFT\go1.18.windows-amd64\go) has no effect
    3. Type: goroutine                  
    4. Time: Jul 6, 2022 at 8:43am (CST)
    5. Entering interactive mode (type "help" for commands, "o" for options)
     
    

    执行Top命令

    1. (pprof) top                                                          
    2. Showing nodes accounting for 102, 100% of 102 total                                                  
    3. Showing top 10 nodes out of 16                                                                        
    4.      flat  flat%   sum%        cum   cum%                                                            
    5.       101 99.02% 99.02%        101 99.02%  runtime.gopark                                            
    6.         1  0.98%   100%          1  0.98%  runtime/pprof.runtime_goroutineProfileWithLabels          
    7.         0     0%   100%          1  0.98%  go-in-practice/code/charpter-01.TestGoroutinePprof        
    8.         0     0%   100%        100 98.04%  go-in-practice/code/charpter-01.TestGoroutinePprof.func1.1
    9.         0     0%   100%          1  0.98%  main.main                                                
    10.         0     0%   100%        101 99.02%  runtime.chanrecv                                          
    11.         0     0%   100%        101 99.02%  runtime.chanrecv1                                        
    12.         0     0%   100%          1  0.98%  runtime.main
    13.         0     0%   100%          1  0.98%  runtime/pprof.(*Profile).WriteTo
    14.         0     0%   100%          1  0.98%  runtime/pprof.writeGoroutine
     
    

    Showing nodes accounting for 102, 100% of 102 total

    此时共有102个协程

    其中

    1. 101 99.02% 99.02% 101 99.02% runtime.gopark 1 0.98% 100%
    2. 1 0.98% runtime/pprof.runtime_goroutineProfileWithLabels

    查看第一行的101个协程的具体调用栈信息

    1. (pprof) traces gopark
    2. Type: goroutine    
    3. Time: Jul 6, 2022 at 8:43am (CST)                                  
    4. -----------+-------------------------------------------------------
    5.       100   runtime.gopark                                        
    6.             runtime.chanrecv                                      
    7.             runtime.chanrecv1                                    
    8.             go-in-practice/code/charpter-01.TestGoroutinePprof.func1.1
    9. -----------+-------------------------------------------------------
    10.         1   runtime.gopark
    11.             runtime.chanrecv
    12.             runtime.chanrecv1
    13.             testing.(*T).Run
    14.             testing.runTests.func1
    15.             testing.tRunner
    16.             testing.runTests
    17.             testing.(*M).Run
    18.             main.main
    19.             runtime.main
    20. -----------+-------------------------------------------------------
     
    

    通过这里大致可以看到101分为两个部分 100 来着于charpter-01.TestGoroutinePprof.func1.1 和 1来自于 runtime.main主协程(本演示案例使用go test进行运行的,所有主协程即为testing.(*M).Run)

    具体查看100个协程的相关源头

    1. (pprof) list func1.1
    2. Total: 102
    3. ROUTINE ======================== go-in-practice/code/charpter-01.TestGoroutinePprof.func1.1 in E:\WORK\PROJECT\git\go\go-in-practice\code\charpter-01\lesson02_test.go
    4.         0        100 (flat, cum) 98.04% of Total
    5.         .          .    232:   var exit = make(chan bool)
    6.         .          .    233:
    7.         .          .    234:   for idx := 1; idx <= goroutine; idx++ {
    8.         .          .    235:           func(i int) {
    9.         .          .    236:                   go func() {
    10.         .        100    237:                           <-exit
    11.         .          .    238:                           wg.Done()
    12.         .          .    239:                           fmt.Printf("No %d exit\n", i)
    13.         .          .    240:                   }()
    14.         .          .    241:           }(idx)
    15.         .          .    242:   }
     
    

    在第一个分析段落里,我们共看到了102个协程,以上由我们的测试代码已经找到了101个协程的来历; 还有一个在哪里呢?

    继续分析;找到第一个分析段落里的1个协程的

    1 0.98% 100% 1 0.98% runtime/pprof.runtime_goroutineProfileWithLabels

    在pprof交互命令行里执行traces runtime_goroutineProfileWithLabels

    1. (pprof) traces runtime_goroutineProfileWithLabels
    2. Type: goroutine
    3. Time: Jul 6, 2022 at 8:43am (CST)
    4. -----------+-------------------------------------------------------
    5.         1   runtime/pprof.runtime_goroutineProfileWithLabels
    6.             runtime/pprof.writeRuntimeProfile
    7.             runtime/pprof.writeGoroutine
    8.             runtime/pprof.(*Profile).WriteTo
    9.             go-in-practice/code/charpter-01.TestGoroutinePprof
    10.             testing.tRunner
    11. -----------+-------------------------------------------------------

    通过这里可以看到,是在我们执行routine采样的代码,产生了一个新的协程

    在pprof交互命令行里执行list runtime_goroutineProfileWithLabels查看源码调用

    1. (pprof) list runtime_goroutineProfileWithLabels  
    2. Total: 102
    3. ROUTINE ======================== runtime/pprof.runtime_goroutineProfileWithLabels in E:\WORK\SOFT\go1.18.windows-amd64\go\src\runtime\mprof.go
    4.         1          1 (flat, cum)  0.98% of Total
    5.         .          .    748:   return
    6.         .          .    749:}
    7.         .          .    750:
    8.         .          .    751://go:linkname runtime_goroutineProfileWithLabels runtime/pprof.runtime_goroutineProfileWithLabels
    9.         .          .    752:func runtime_goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
    10.         1          1    753:   return goroutineProfileWithLabels(p, labels)
    11.         .          .    754:}
    12.         .          .    755:
    13.         .          .    756:// labels may be nil. If labels is non-nil, it must have the same length as p.
    14.         .          .    757:func goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
    15.         .          .    758:   if labels != nil && len(labels) != len(p) {

    在这里启动了新的协程;

    以上是对第一个采样点的分析;第一个分析点,是在退出信号没有发出的时候,进行采样的,我们创建的100个协程都没有退出; 下面来看看第二个分析点,第二个分析点,是在主协程已经给所有的子协程发出退出信号后,并且等待并确保所有100子协程都全部退出后,在主协程里进行goroutine采样;

    通过pprof对第二个采样数据进行分析

    1. PS E:\WORK\PROJECT\git\go\go-in-practice\code\charpter-01> go tool pprof goroutine-prof-2
    2. warning: GOPATH set to GOROOT (E:\WORK\SOFT\go1.18.windows-amd64\go) has no effect
    3. Type: goroutine                  
    4. Time: Jul 6, 2022 at 8:43am (CST)
    5. Entering interactive mode (type "help" for commands, "o" for options)

    执行Top命令

    1. (pprof) top                                                          
    2. Showing nodes accounting for 2, 100% of 2 total          
    3. Showing top 10 nodes out of 15                            
    4.      flat  flat%   sum%        cum   cum%                
    5.         1 50.00% 50.00%          1 50.00%  runtime.gopark
    6.         1 50.00%   100%          1 50.00%  runtime/pprof.runtime_goroutineProfileWithLabels
    7.         0     0%   100%          1 50.00%  go-in-practice/code/charpter-01.TestGoroutinePprof
    8.         0     0%   100%          1 50.00%  main.main
    9.         0     0%   100%          1 50.00%  runtime.chanrecv
    10.         0     0%   100%          1 50.00%  runtime.chanrecv1
    11.         0     0%   100%          1 50.00%  runtime.main
    12.         0     0%   100%          1 50.00%  runtime/pprof.(*Profile).WriteTo
    13.         0     0%   100%          1 50.00%  runtime/pprof.writeGoroutine
    14.         0     0%   100%          1 50.00%  runtime/pprof.writeRuntimeProfile

    Showing nodes accounting for 2, 100% of 2 total

    此时共有2个协程

    其中

    1. 1 50.00% 50.00% 1 50.00% runtime.gopark
    2. 1 50.00% 100% 1 50.00% runtime/pprof.runtime_goroutineProfileWithLabels

    按照第一轮的分析方法;我们可以看到一个是主协程,一个是采样数据的新启动的协程

    使用图形化web命令进行分析

    在pprof的交互模式里输入web命令

    下图为第一次采样点对应的图形化

    下图为第二次采样点对应的图形化

    图形化的结果很直观,和trace的结果是一致的; 在无法安装图形化的情况下;可以通过traces来查看类似图形调用树的结果

    使用traces命令查看采样数据

    1. (pprof) traces gopark
    2. Type: goroutine
    3. Time: Jul 6, 2022 at 10:35am (CST)
    4. -----------+-------------------------------------------------------
    5.         1   runtime.gopark
    6.             runtime.chanrecv
    7.             runtime.chanrecv1
    8.             testing.(*T).Run
    9.             testing.runTests.func1
    10.             testing.tRunner
    11.             testing.runTests
    12.             testing.(*M).Run
    13.             main.main
    14.             runtime.main
    15. -----------+-------------------------------------------------------
    ​
    ​

    通过pprof我们进行goroutine的分析;本文中的例子;仅仅是笔者用来演示分析过程只用, 在项目过程中,都是通过压力测试的场景下,查看goroutine的资源情况,然后采样,在进行分析数据; 用以上的分析过程,同样的适用于这样的场景。

  • 相关阅读:
    Java版分布式微服务云开发架构 Spring Cloud+Spring Boot+Mybatis 电子招标采购系统功能清单
    基于布谷鸟优化的BP神经网络(预测应用) - 附代码
    动态内存管理
    雨水情自动监测 遥测终端机
    又拍云之 Keepalived 高可用部署
    Hive概念及Hive,MySQL安装、配置
    ARM异常处理(2):中断的输入和挂起的6种情况分析
    爬虫----js逆向某宝h5的sign参数
    python爬虫之js逆向入门:常用加密算法的逆向和实践
    【Java】基于物联网技术的智慧工地源码(项目端、监管端、APP端、智慧大屏)
  • 原文地址:https://blog.csdn.net/inthirties/article/details/125635316