goroutine 作为 golang 并发实现的外围组成部分,非常容易上手应用,但却很难驾驭得好。咱们常常会遭逢各种模式的 goroutine 透露,这些透露的 goroutine 会始终存活直到过程终结。它们的占用的栈内存始终无奈开释、关联的堆内存也不能被GC清理,零碎的可用内存会随透露 goroutine 的增多越来越少,直至解体!
Uber开源了goleak库能够帮忙咱们检测代码中可能存在的goroutine泄露问题;
源代码:
https://github.com/JasonkayZK...
应用Uber开源的goleak库进行goroutine泄露检测
什么是Goroutine泄露
goroutine leak 的意思是go协程透露,那么什么又是协程透露呢?
咱们晓得,每次应用go关键字开启一个gorountine工作,通过一段时间的运行,最终是会完结,从而进行系统资源的开释回收。而如果因为操作不当导致一些goroutine始终处于阻塞状态或者永远运行中,永远也不会完结,这就必定会始终占用系统资源;最坏的状况下是随着零碎运行,始终在创立此类goroutine,那么最终后果就是程序解体或者零碎解体;这种状况咱们个别称为goroutine leak;
例如上面的一段代码:
package mainimport ( "fmt" "math/rand" "runtime" "time" )func query() int { n := rand.Intn(100) time.Sleep(time.Duration(n) * time.Millisecond) return n }// 每次执行此函数,都会导致有两个goroutine处于阻塞状态func queryAll() int { ch := make(chan int) go func() { ch <- query() }() go func() { ch <- query() }() go func() { ch <- query() }() // <-ch // <-ch return <-ch }func main() { // 每次循环都会透露两个goroutine for i := 0; i < 4; i++ { queryAll() // main()也是一个主groutine fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine()) } }
输入如下:
#goroutines: 3#goroutines: 5#goroutines: 7#goroutines: 9
这里发现goroutine的数量始终在增涨,按理说这里的值应该始终是 1 才对(只有一个Main 函数的主goroutine),其实这里产生了goroutine透露的问题:
次要问题产生在 queryAll() 函数里,这个函数在goroutine里往ch里间断三次写入了值,因为这里是无缓冲的ch,所以在写入值的时候,要有在ch有接收者时才能够写入胜利,也就是说在从接收者从ch中获取值之前, 后面三个ch<-query() 始终处于阻塞的状态;
当执行到queryAll()函数的 return语句时,ch接收者获取一个值(意思是说三个ch<-query() 中执行最快的那个goroutine写值到ch胜利了,还剩下两个执行慢的 ch<-query() 处于阻塞)并返回给调用主函数时,仍有两个ch处于节约的状态;
在Main函数中对于for循环:
- 第一次:goroutine的总数量为 1个主goroutine + 2个节约的goroutine = 3;
- 第二次:3 + 再个节约的2个goroutine = 5;
- 第三次:5 + 再个节约的2个goroutine = 7;
- 第三次:7 + 再个节约的2个goroutine = 9;
正好是程序的输入后果;
解决方案:
能够看到,次要是ch写入值次数与读取的值的次数不统一导致的有ch始终处于阻塞节约的状> 态,咱们所以咱们只有保留写与读的次数齐全一样就能够了;
这里咱们把下面queryAll() 函数代码正文掉的 <-ch 两行勾销掉,再执行就失常了,输入内容如下:
#goroutines: 1
#goroutines: 1
#goroutines: 1
#goroutines: 1
当然对于解决goroutine的办法不是仅仅这一种,也能够利用context来解决,参考:
https://www.cnblogs.com/chenq...
总结如下:
产生goroutine leak的起因
- goroutine因为channel的读/写端退出而始终阻塞,导致goroutine始终占用资源,而无奈退出,如只有写入,没有接管,反之一样;
goroutine进入死循环中,导致资源始终无奈开释;
揭示:垃圾收集器不会收集以下模式的goroutines:go func() {// <操作会在这里永恒阻塞>}()// Do work
这个goroutine将始终存在,直到整个程序退出;
是否属于goroutine leak 还须要看如何应用了,如https://www.jianshu.com/p/b52...。如果解决不好,如for{} 基本就不可能完结,此时就属于透露;
所以咱们写程序时,至多要保障他们完结的条件,且肯定能够完结才算失常;
- goroutine终止的场景
- goroutine实现它的工作
- 因为产生了没有解决的谬误
- 收到完结信号,间接终止工作
能够看进去,goroutine 的透露通常随同着简单的协程间通信,而代码评审和惯例的单元测试通常更专一于业务逻辑正确,很难齐全笼罩 goroutine 透露的场景;
同时,pprof 等性能剖析工具更多是作用于监控报警/故障之后的复盘。咱们须要一款能在编译部署前辨认 goroutine 透露的工具,从更上游把控工程质量;
goleak 是 Uber 团队开源的一款 goroutine 透露检测工具,它能够十分轻量地集成到测试中,对于 goroutine 透露的防治和工程鲁棒性的晋升很有帮忙;
应用 uber-go/goleak 工具检测goleak
下面咱们是手动通过获取 groutine数量来判断是否存在透露的,上面咱们应用 uber-go/goleak工具来检测是否存在透露问题;
上面的函数在每次调用时都会造成一个goroutine泄露:
func leak() { ch := make(chan struct{}) go func() { ch <- struct{}{} }()}
通常咱们会为 leak函数写相似上面的测试:
func TestLeak(t *testing.T) { leak()}
用 go test 执行测试看看后果:
$ go test -v -run ^TestLeak$=== RUN TestLeak--- PASS: TestLeak (0.00s)PASSok cool-go.gocn.vip/goleak 0.007s
测试不出意外地顺利通过了!
go 内置的测试显然无奈帮咱们辨认 leak中的 goroutine 透露!
在应用goleak进行goroutine泄露测试时,通常咱们只需关注 VerifyNone
和 VerifyTestMain
两个办法,它们也对应了 goleak 的两种集成形式;
随用例集成
在现有测试的首行增加 defer goleak.VerifyNone(t),即可集成 goleak 透露检测:
func TestLeakWithGoleak(t *testing.T) { defer goleak.VerifyNone(t) leak()}
这次的 go test 失败了:
$ go test -v -run ^TestLeakWithGoleak$=== RUN TestLeakWithGoleak leaks.go:78: found unexpected goroutines: [Goroutine 19 in state chan send, with cool-go.gocn.vip/goleak.leak.func1 on top of the stack: goroutine 19 [chan send]: cool-go.gocn.vip/goleak.leak.func1(0xc00008c420) /Users/blanet/gocn/goleak/main.go:24 +0x35 created by cool-go.gocn.vip/goleak.leak /Users/blanet/gocn/goleak/main.go:23 +0x4e ]--- FAIL: TestLeakWithGoleak (0.45s)FAILexit status 1FAIL cool-go.gocn.vip/goleak 0.459s
测试报告显示名为 leak.func1 的 goroutine 产生了透露(leak.func1 在这里指的是 leak 办法中的第一个匿名办法),并将测试后果置为失败;
咱们胜利通过 goleak 找到了 goroutine 透露;
通过 TestMain 集成
如果感觉逐用例集成 goleak 的形式太过繁琐或 “入侵” 性太强,能够试试齐全不扭转原有测试用例,通过在 TestMain中增加 goleak.VerifyTestMain(m) 的形式集成 goleak:
func TestMain(m *testing.M) { goleak.VerifyTestMain(m)}
这次的 go test 输入如下:
$ go test -v -run ^TestLeak$=== RUN TestLeak--- PASS: TestLeak (0.00s)PASSgoleak: Errors on successful test run: found unexpected goroutines:[Goroutine 19 in state chan send, with cool-go.gocn.vip/goleak.leak.func1 on top of the stack:goroutine 19 [chan send]:cool-go.gocn.vip/goleak.leak.func1(0xc00008c2a0) /Users/blanet/gocn/goleak/main.go:24 +0x35created by cool-go.gocn.vip/goleak.leak /Users/blanet/gocn/goleak/main.go:23 +0x4e]exit status 1FAIL cool-go.gocn.vip/goleak 0.455s
可见,goleak 再次胜利检测到了 goroutine 透露;
但与逐用例集成不同的是,goleak.VerifyTestMain会先报告用例执行的后果,而后再进行透露剖析;
同时,如果单次测试执行了多个用例且最终产生透露,那么以 TestMain 形式集成的 goleak 并不能精准定位产生 goroutine 透露的用例,还需进一步剖析;
goleak 提供了如下脚本用于进一步推断具体产生 goroutine 透露的用例,其本质是逐个执行所有用例进行剖析:
# Create a test binary which will be used to run each test individually$ go test -c -o tests# Run each test individually, printing "." for successful tests, or the test name# for failing tests.$ for test in $(go test -list . | grep -E "^(Test|Example)"); do ./tests -test.run "^$test\$" &>/dev/null && echo -n "." || echo "\n$test failed"done
总结
goleak 通过对运行时的栈剖析获取 goroutine 状态,并设计了十分简洁易用的接口与测试框架进行对接,是一款玲珑强悍的 goroutine 透露防治利器;
当然,齐备的测试用例反对是 goleak 发挥作用的根底,大家还是要老老实实写测试,稳稳当当搞生产!
附录
文章参考:
https://mp.weixin.qq.com/s/3i...
https://blog.haohtml.com/arch...
源代码:https://github.com/JasonkayZK...
原文地址 > https://jasonkayzk.github.io/...
关注 golang技术实验室
获取更多好文
本文由mdnice多平台公布