Go語言Goroutines?泄漏場景與防治解決分析
場景
Go 有很多自動管理內存的功能。比如:
- 變量分配到堆內存還是棧內存,編譯器會通過逃逸分析(escpage analysis)來判斷;
- 堆內存的垃圾自動回收。
即便如此,如果編碼不謹慎,我們還是有可能導致內存泄漏的,最常見的是 goroutine 泄漏,比如下面的函數:
func goroutinueLeak() {
ch := make(chan int)
go func(ch chan int) {
// 因為 ch 一直沒有數據,所以這個協程會阻塞在這里。
val := <-ch
fmt.Println(val)
}(ch)
}
由于ch一直沒有發(fā)送數據,所以我們開啟的 goroutine 會一直阻塞。每次調用goroutinueLeak都會泄漏一個goroutine,從監(jiān)控面板看到話,goroutinue 數量會逐步上升,直至服務 OOM。
Goroutine 泄漏常見原因
channel 發(fā)送端導致阻塞
使用 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 <-ctx.Done():
return errors.New("timeout")
case val := <-ch:
fmt.Println(val)
}
return nil
}
RetriveData() 如果超時了,那么contextLeak() 會返回 error,本函數執(zhí)行結束。而我們開啟的協程g1,由于沒有接受者,會阻塞在 ch<-val。
解決方法也能簡單,比如可以給ch加上緩存。
channel 接收端導致阻塞
開篇給出的函數goroutinueLeak,就是因為channel的接收端收不到數據,導致阻塞。
這里舉出另一個例子,下面的函數,是否有可能 goroutinue 泄漏?
func errorAssertionLeak() {
ch := make(chan int)
// g1
go func() {
val := <-ch
fmt.Println(val)
}()
// RetriveSomeData 表示獲取數據,比如從網絡上
val, err := RetriveSomeData()
if err != nil {
return
}
ch <- val
return nil
}
如果 RetriveSomeData() 返回的err 不為 nil,那么本函數中斷,也就不會有數據發(fā)送給ch,這導致協程g1會一直阻塞。
如何預防
goroutine 泄漏往往需要服務運行一段時間后,才會被發(fā)覺。
我們可以通過監(jiān)控 goroutine 數量來判斷是否有 goroutine 泄漏;或者用 pprof(之前文章介紹過的) 來定位泄漏的 goroutine。但這些已經是亡羊補牢了。最理想的情況是,我們在開發(fā)的過程中,就能發(fā)現。
本文推薦的做法是,使用單元測試。以開篇的 goroutinueLeak 為例子,我們寫個單測:
func TestLeak(t *testing.T) {
goroutinueLeak()
}
執(zhí)行 go test,發(fā)現測試是通過的:
=== RUN TestLeak
--- PASS: TestLeak (0.00s)
PASS
ok 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()
}
這時候執(zhí)行 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)
FAIL
FAIL 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)
}
總結
雖然我的文章經常提及單測,但我本人不是單元測試的忠實粉絲。扎實的基礎,充分的測試,負責任的態(tài)度也是非常重要的。
引用
以上就是Go語言Goroutines 泄漏場景與防治解決分析的詳細內容,更多關于Go Goroutines 泄漏防治的資料請關注腳本之家其它相關文章!
相關文章
go語言開發(fā)環(huán)境配置(sublime text3+gosublime)
網上google了下go的開發(fā)工具,大都推薦sublime text3+gosublime,本文就介紹了go語言開發(fā)環(huán)境配置(sublime text3+gosublime),具有一定的參考價值,感興趣的可以了解一下2022-01-01

