Goroutine 泄漏防治神器 goleak
推薦 goleak 的背景
goroutine 作為 golang 并發實現的核心組成部分,非常容易上手使用,但卻很難駕馭得好。我們經常會遭遇各種形式的 goroutine 泄漏,這些泄漏的 goroutine 會一直存活直到進程終結。它們的占用的棧內存一直無法釋放、關聯的堆內存也不能被 GC 清理,系統的可用內存會隨泄漏 goroutine 的增多越來越少,直至崩潰!

goroutine 的泄漏通常伴隨著復雜的協程間通信,代碼評審和常規的單元測試通常更專注于業務邏輯正確,很難完全覆蓋 goroutine 泄漏的場景;而 pprof 等性能分析工具更多是作用于監控報警/故障之后的復盤。我們需要一款能在編譯部署前識別 goroutine 泄漏的工具,從更上游把控工程質量。
goleak(https://github.com/uber-go/goleak MIT 許可協議) 是 Uber 團隊開源的一款 goroutine 泄漏檢測工具,它可以非常輕量地集成到測試中,對于 goroutine 泄漏的防治和工程魯棒性的提升很有幫助。
防范勝于救災
goroutine 泄漏舉例
先舉個 goroutine 泄漏的例子;如下所示,leak 方法中的 ch 永遠沒有讀操作且不會關閉,寫入 ch 的 goroutine 一直處于阻塞狀態,這是一種很典型的 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) PASS ok cool-go.gocn.vip/goleak 0.007s
測試不出意外地順利通過了,go 內置的測試顯然無法幫我們識別 leak中的 goroutine 泄漏。
集成 goleak 測試
goleak 暴露的方法特別精簡,通常我們只需關注 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) FAIL exit status 1 FAIL 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) PASS goleak: 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 +0x35 created by cool-go.gocn.vip/goleak.leak /Users/blanet/gocn/goleak/main.go:23 +0x4e ] exit status 1 FAIL 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 發揮作用的基礎,大家還是要老老實實寫測試,穩穩當當搞生產!