51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 881|回复: 0
打印 上一主题 下一主题

[转贴] 如何在测试中发现Goroutine泄漏

[复制链接]
  • TA的每日心情
    擦汗
    昨天 09:02
  • 签到天数: 1046 天

    连续签到: 4 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2022-5-19 09:42:17 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    前言
      哈喽,大家好,我是asong;
      众所周知,gorourtine的设计是Go语言并发实现的核心组成部分,易上手,但是也会遭遇各种疑难杂症,其中[url=]goroutine[/url]泄漏就是重症之一,其出现往往需要排查很久,有人说可以使用pprof来排查,虽然其可以达到目的,但是这些性能分析工具往往是在出现问题后借助其辅助排查使用的,有没有一款可以防患于未然的工具吗?当然有,goleak他来了,其由 Uber 团队开源,可以用来检测goroutine泄漏,并且可以结合单元测试,可以达到防范于未然的目的,本文我们就一起来看一看goleak。
      goroutine泄漏
      不知道你们在日常开发中是否有遇到过goroutine泄漏,goroutine泄漏其实就是goroutine阻塞,这些阻塞的goroutine会一直存活直到进程终结,他们占用的栈内存一直无法释放,从而导致系统的可用内存会越来越少,直至崩溃!简单总结了几种常见的泄漏原因:
      ·Goroutine内的逻辑进入死循坏,一直占用资源
      · Goroutine配合channel/mutex使用时,由于使用不当导致一直被阻塞
      · Goroutine内的逻辑长时间等待,导致Goroutine数量暴增
      接下来我们使用Goroutine+channel的经典组合来展示goroutine泄漏;
    1. func GetData() {
    2.    var ch chan struct{}
    3.    go func() {
    4.     <- ch
    5.    }()
    6.   }
    7.   func main()  {
    8.    defer func() {
    9.     fmt.Println("goroutines: ", runtime.NumGoroutine())
    10.    }()
    11.    GetData()
    12.    time.Sleep(2 * time.Second)
    13.   }
    复制代码
    这个例子是channel忘记初始化,无论是读写操作都会造成阻塞,这个方法如果是写单测是检查不出来问题的:

    1. func TestGetData(t *testing.T) {
    2.    GetData()
    3.   }
    复制代码
    运行结果:

    1. === RUN   TestGetData
    2.   --- PASS: TestGetData (0.00s)
    3.   PASS
    复制代码
    内置测试无法满足,接下来我们引入goleak来测试一下。
      goleak
      github地址:https://github.com/uber-go/goleak
      使用goleak主要关注两个方法即可:VerifyNone、VerifyTestMain,VerifyNone用于单一测试用例中测试,VerifyTestMain可以在TestMain中添加,可以减少对测试代码的入侵,举例如下:
      使用VerifyNone:
    1. func TestGetDataWithGoleak(t *testing.T) {
    2.    defer goleak.VerifyNone(t)
    3.    GetData()
    4.   }
    复制代码
    运行结果:
    1. === RUN   TestGetDataWithGoleak
    2.       leaks.go:78: found unexpected goroutines:
    3.           [Goroutine 35 in state chan receive (nil chan), with asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1 on top of the stack:
    4.           goroutine 35 [chan receive (nil chan)]:
    5.           asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1()
    6.            /Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:12 +0x1f
    7.           created by asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData
    8.            /Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:11 +0x3c
    9.           ]
    10.   --- FAIL: TestGetDataWithGoleak (0.45s)
    11.   FAIL
    12.   Process finished with the exit code 1
    复制代码
    通过运行结果看到具体发生goroutine泄漏的具体代码段;使用VerifyNone会对我们的测试代码有入侵,可以采用VerifyTestMain方法可以更快的集成到测试中:
    1.  func TestMain(m *testing.M) {
    2.    goleak.VerifyTestMain(m)
    3.   }
    复制代码
    运行结果:
    1.  === RUN   TestGetData
    2.   --- PASS: TestGetData (0.00s)
    3.   PASS
    4.   goleak: Errors on successful test run: found unexpected goroutines:
    5.   [Goroutine 5 in state chan receive (nil chan), with asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1 on top of the stack:
    6.   goroutine 5 [chan receive (nil chan)]:
    7.   asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1()
    8.    /Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:12 +0x1f
    9.   created by asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData
    10.    /Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:11 +0x3c
    11.   ]
    12.   Process finished with the exit code 1
    复制代码
    VerifyTestMain的运行结果与VerifyNone有一点不同,VerifyTestMain会先报告测试用例执行结果,然后报告泄漏分析,如果测试的用例中有多个goroutine泄漏,无法精确定位到发生泄漏的具体test,需要使用如下脚本进一步分析:
    1. # Create a test binary which will be used to run each test individually
    2.   $ go test -c -o tests
    3.   # Run each test individually, printing "." for successful tests, or the test name
    4.   # for failing tests.
    5.   $ for test in $(go test -list . | grep -E "^(Test|Example)"); do ./tests -test.run "^$test\$" &>/dev/null && echo -n "." || echo -e "\n$test failed"; done
    复制代码
    这样会打印出具体哪个测试用例失败。
      goleak实现原理
      从VerifyNone入口,我们查看源代码,其调用了Find方法:
    1.  // Find looks for extra goroutines, and returns a descriptive error if
    2.   // any are found.
    3.   func Find(options ...Option) error {
    4.     // 获取当前goroutine的ID
    5.    cur := stack.Current().ID()
    6.    opts := buildOpts(options...)
    7.    var stacks []stack.Stack
    8.    retry := true
    9.    for i := 0; retry; i++ {
    10.       // 过滤无用的goroutine
    11.     stacks = filterStacks(stack.All(), cur, opts)
    12.     if len(stacks) == 0 {
    13.      return nil
    14.     }
    15.     retry = opts.retry(i)
    16.    }
    17.    return fmt.Errorf("found unexpected goroutines:\n%s", stacks)
    18.   }
    复制代码
    我们在看一下filterStacks方法:
    1.  // filterStacks will filter any stacks excluded by the given opts.
    2.   // filterStacks modifies the passed in stacks slice.
    3.   func filterStacks(stacks []stack.Stack, skipID int, opts *opts) []stack.Stack {
    4.    filtered := stacks[:0]
    5.    for _, stack := range stacks {
    6.     // Always skip the running goroutine.
    7.     if stack.ID() == skipID {
    8.      continue
    9.     }
    10.     // Run any default or user-specified filters.
    11.     if opts.filter(stack) {
    12.      continue
    13.     }
    14.     filtered = append(filtered, stack)
    15.    }
    16.    return filtered
    17.   }
    复制代码
    这里主要是过滤掉一些不参与检测的goroutine stack,如果没有自定义filters,则使用默认的filters:
    1. func buildOpts(options ...Option) *opts {
    2.    opts := &opts{
    3.     maxRetries: _defaultRetries,
    4.     maxSleep:   100 * time.Millisecond,
    5.    }
    6.    opts.filters = append(opts.filters,
    7.     isTestStack,
    8.     isSyscallStack,
    9.     isStdLibStack,
    10.     isTraceStack,
    11.    )
    12.    for _, option := range options {
    13.     option.apply(opts)
    14.    }
    15.    return opts
    16.   }
    复制代码
    从这里可以看出,默认检测20次,每次默认间隔100ms;添加默认filters;
      总结一下goleak的实现原理:
      使用runtime.Stack()方法获取当前运行的所有goroutine的栈信息,默认定义不需要检测的过滤项,默认定义检测次数+检测间隔,不断周期进行检测,最终在多次检查后仍没有找到剩下的goroutine则判断没有发生goroutine泄漏。
      总结
      本文我们分享了一个可以在测试中发现goroutine泄漏的工具,但是其还是需要完备的测试用例支持,这就暴露出测试用例的重要性,朋友们好的工具可以助我们更快的发现问题,但是代码质量还是掌握在我们自己的手中,加油吧,少年们~




    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏1
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-15 01:45 , Processed in 0.060826 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表