前言大家好,我是asong;众所周知,goroutine的设计是Go语言并发实现的核心组件。使用方便,但也会遇到各种疑难杂症。Goroutine泄漏是严重的疾病之一。检查它的发生通常需要很长时间。有人说pprof可以用来查。虽然可以达到目的,但是这些性能分析工具往往是在出现问题后辅助排查问题。有吗?有没有一种工具可以防患于未然?当然,这里有goleak。它由Uber团队开源,可用于检测goroutine泄漏。也可以结合单元测试,达到防患于未然的目的。这篇文章我们来看看goleak。Goroutineleaks不知道大家在日常开发中有没有遇到过goroutineleaks。Goroutine泄漏实际上是goroutine阻塞。这些被阻塞的goroutines会一直存活到进程结束。它们占用的栈内存无法释放,导致系统可用内存不足。会越来越少,直到崩溃!简单总结几种常见的泄漏原因:Goroutine中的逻辑进入死循环,一直在占用资源。当Goroutine与channel/mutex一起使用时,由于使用不当而被阻塞。等待时间过长,导致Goroutine数量激增接下来,我们使用Goroutine+channel的经典组合来展示goroutine泄漏;funcGetData(){varchchanstruct{}gofunc(){<-ch}()}funcmain(){deferfunc(){fmt.Println("goroutines:",runtime.NumGoroutine())}()GetData()time.Sleep(2*time.Second)}这个例子是channel忘记初始化,不管是读操作还是写操作都会造成阻塞。如果这个方法写在单个测试中,无法检查出问题:funcTestGetData(t*testing.T){GetData()}运行结果:===RUNTestGetData---PASS:TestGetData(0.00s)PASSbuilt-intest不能满足,那么我们引入goleak来测试一下。goleakgithub地址:https://github.com/uber-go/goleak使用goleak主要关注两个方法:VerifyNone、VerifyTestMain,VerifyNone用于在单个测试用例中进行测试,在TestMain中可以添加VerifyTestMain,可以减少需要测试代码入侵,例如:UseVerifyNone:funcTestGetDataWithGoleak(t*testing.T){defergoleak.VerifyNone(t)GetData()}运行结果:===RUNTestGetDataWithGoleakleaks.go:78:foundunexpectedgoroutines:[Goroutine35处于状态chanreceive(nilchan),withasong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1在栈顶:goroutine35[chanreceive(nilchan)]:asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1()/Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:12+0x1f创建者asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData/Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:11+0x3c]---失败:TestGetDataWithGoleak(0.45s)FAIL进程结束hedwithexitcode1可以通过运行结果看到goroutineleak发生的具体代码段;使用VerifyNone会侵入我们的测试代码,使用VerifyTestMain方法可以更快的融入测试:funcTestMain(m*testing.M){goleak.VerifyTestMain(m)}运行结果:===RUNTestGetData---PASS:TestGetData(0.00s)PASSgoleak:成功测试运行时出错:发现意外的goroutines:[状态为chanreceive(nilchan)的Goroutine5,堆栈顶部有asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1:goroutine5[chanreceive(nilchan)]:asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1()/Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:12+0x1fcreatedbyasong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData/Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:11+0x3c]Processfinishedwithexitcode1VerifyTestMain运行结果与VerifyNone略有不同。VerifyTestMain会先上报测试用例执行结果,再上报泄漏分析。如果测试用例中存在多个goroutine泄漏,无法准确定位泄漏发生的具体测试,需要使用如下脚本进一步分析:#创建一个测试二进制文件,用于单独运行每个测试$gotest-c-otests#单独运行每个测试,打印“.”对于成功的测试,或测试失败的测试名称#。$用于$(gotest-list.|grep-E"^(Test|Example)")中的测试;做./tests-test.run"^$test\$"&>/dev/null&&echo-n"."||echo-e"\n$测试失败";done这将打印出哪个测试用例失败了goleak的实现原理从VerifyNone说起。我们查看源码,其中调用了Find方法://Find查找额外的goroutines,如果找到则返回描述性错误。funcFind(options...Option)error{//获取goroutine的ID当前goroutinecur:=stack.Current().ID()opts:=buildOpts(options...)varstacks[]stack.Stackretry:=truefori:=0;重试;i++{//过滤无用的goroutinestacks=filterStacks(stack.All(),cur,opts)iflen(stacks)==0{returnnil}retry=opts.retry(i)}returnfmt.Errorf("foundunexpectedgoroutines:\n%s",stacks)}我们正在查看filterStacks方法://filterStacks将过滤给定选项排除的任何堆栈。//filterStacks修改传入的栈slice.funcfilterStacks(stacks[]stack.Stack,skipIDint,opts*opts)[]stack.Stack{filtered:=stacks[:0]for_,stack:=rangestacks{//总是跳过正在运行的goroutine。ifstack.ID()==skipID{continue}//运行任何默认或用户指定的过滤器。如果选择。过滤器(堆栈){继续}过滤=附加d(filtered,stack)}returnfiltered}这里的主要目的是过滤掉一些不参与检测的goroutine栈。如果没有自定义过滤器,则使用默认过滤器:funcbuildOpts(options...Option)*opts{opts:=&opts{maxRetries:_defaultRetries,maxSleep:100*time.Millisecond,}opts.filters=append(opts.filters,isTestStack,isSyscallStack,isStdLibStack,isTraceStack,)for_,option:=rangeoptions{option.apply(opts)}returnopts}从这里可以看出默认检测20次,默认间隔100ms每一次;添加默认过滤器;总结一下goleak的实现原理:使用runtime.Stack()方法获取当前运行的所有goroutine的栈信息,默认定义不需要检测的过滤项,检测次数+检测间隔为默认定义,并连续执行检测。如果多次检查都没有发现剩余的goroutine,则判断没有goroutine泄漏。一个goroutine泄漏的工具,但它仍然需要完整的测试用例支持,这暴露了测试用例的重要性。朋友们,好的工具可以帮助我们更快的发现问题,但是代码质量还是掌握在我们自己手中。加油吧少年们~好了,这篇文章就到这里了,我是asong,我们下期再见。
