Go语言Goroutines 泄漏场景与防治解决分析
目录
- 场景
- Goroutine 泄漏常见原因
- channel 发送端导致阻塞
- channel 接收端导致阻塞
- 如何预防 开发者_JAVA学习
- 总结
场景
Go 有很多自动管理内存的功能。比如:
- 变量分配到堆内存还是栈内存,编译器会通过逃逸分析(escpage analysis)来判断;
- 堆内存的垃圾自动编程客栈回收。
即便如此,如果编码不谨慎,我们还是有可能导致内存泄漏的,最常见的是 goroutine 泄漏,比如下面的函数:
func goroutinueLeak() { ch := make(chan int) go func(ch chan int) { // 因为 ch 一直没有数据,所以这个协程会阻塞在这里。 val := <-ch fmt.Println(val) }(ch) }
由于ch
一直没有发送数据,所以我们开启的 goroutine 会一直阻塞。每次调用goroutinueLeak
都会泄漏一个goroutine,从监控面板看到话,goroutinue 数量会逐步上升,直至服务 OOM。
Goroutine 泄漏常见原因
channel 发送端导致阻塞
使用 context 设置超时是常见的一个场景,试想一下,下面的函数什么情况下会 goroutine 泄漏 ?
func contextLeak() error { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() ch := make(chan int) //g1 go func() { // 获取数据,比如网络请求,可能时间很久 val := RetriveData() ch <- val }() select { case <-ctphpx.Done(): return errors.New("timeout") case val := <-ch: fmt.Println(val) } return nil }
RetriveData()
如果超时了,那么contextLeak()
会返回 error,本函数执行结束。而我们开启的协程g1
,由于没有接受者,会阻塞在 ch<-val
。
解决方法也能简单,比如可以给ch
加上缓存。
channel 接收端导致阻塞
开篇给出的函数goroutinueLeak
,就是因为channel的接收端收不到数据,导致阻塞。
这里举出另一个例子,下面的函数,是否有可能 goroutinue 泄漏?
func errorAssertionLeak() { ch := make(chan int) // g1 go func() { vakEgwHwpRCl := <-ch fmt.Println(val) }() // RetriveSomeData 表示获取数据,比如从网络上 val, err := RetriveSomeData() if err != nil { return } ch <- val return nil }
如果 RetriveSomeData()
返回的err
不为 nil
,那么本函数中断,也就不会有数据发送给ch
,这导致协程g1
会一直阻塞。
如何预防
goroutine 泄漏往往需要服务运行一段时间后,才会被发觉。
我们可以通过监控 goroutine 数量来判断是否有 goroutine 泄漏;或者用 pprof(之前文章介绍过的) 来定位泄漏的 goroutine。但这些已经是亡羊补牢了。最理想的情况是,我们在开发的过程中,就能发现。
本文推荐的做法是,使用单元测试。以开篇的 goroutinueLeak
为例子,我们写个单测:
func TestLeak(t *testing.T) { goroutinueLeak() }
执行 go test,发现测试是通过的:
=== RUN TestLeak
--- PASS: TestLeak (0.00s)PASSok example/leak 0.598s
这是是因为单测默认不会检测 goroutine 泄漏的。
我们可以在单测中,加入Uber 团队提供的 uber-go/goleak
包:
import ( "testing" "go.uber.org/goleak" ) func TestLeak(t *testing.T) { // 加上这行代码,就会自动检测是否 goroutine 泄漏 defer goleak.VerifyNone(t) goroutinueLeak() }
这时候执行 go test,输出:
=== RUN TestLeak
/xxx/leak_test.go:12: found unexpected goroutines: [Goroutine 21 in state chan receive, with example/leak.goroutinueLeak.func1 on top of the stack: goroutine 21 [chan receive]: example/leak.goroutinueLeak.func1(0x0) /xxx/leak.go:9 +0x27 created by example/leak.goroutinueLeak /xxx/leak.go:8 +0x7a ]--- FAIL: TestLeak (0.46s)FAILFAIL example/leak 0.784s
这时候单测会因为 goroutine 泄漏而不通过。
如果你觉得每个测试用例都要加上 defer goleak.VerifyNone(t)
太繁琐的话(特别是在已有的项目中加上),goleak 提供了在 TestMain 中使用的方法VerifyTestMain
,上面的单测可以修改成:
func TestLeak(t *testing.T) { goroutinueLeak() } func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }
总结
虽然我的文章经常提及单测,但我编程本人不是单元测试的忠实粉丝。扎实的基础,充分的测试,负责任的态度也是非常重要的。
引用
- www.ardanlabs.com/blog/2018/1…
- www.ardanlabs.com/blkEgwHwpRCog/2018/1…
- github.com/uber-go/gol…
以上就是Go语言Goroutines 泄漏场景与防治解决分析的详细内容,更多关于Go Goroutines 泄漏防治的资料请关注我们其它相关文章!
精彩评论