はじめに
本記事では、テストでゴールーチンリークを検出する便利なライブラリについて紹介し、その仕組について解説します。
ゴールーチンリークとは
ゴールーチンリークとは、ゴールーチンが終了せず、ずっと残ってしまうことを指します。ゴールーチンが終了しないと、メモリも開放されないため、メモリリークが発生します。
たとえば、次のようなコードではゴールーチンリークが発生します。
package main
import (
"fmt"
"runtime"
)
func f() {
ch := make(chan int)
go func() {
for x := range ch {
fmt.Println(x)
}
}()
ch <- 10
ch <- 20
}
func main() {
fmt.Println(runtime.NumGoroutine()) // 1
f()
fmt.Println(runtime.NumGoroutine()) // 2
}
関数f
を呼び出す前とあとでは、runtime.NumGoroutine
関数が返すゴールーチン数が変わります。関数f
内で作成したゴールーチンがリークを起こしていることが分かります。
ゴールーチンリークを解消するには、チャネルch
をクローズして、ゴールーチン内のfor range
文を止めて関数を抜け出す必要があります。
メモリ消費量やゴールーチン数を監視していると、ともに増加していっている場合はリークが起きている可能性があります。監視でリークが見つかった場合は、該当箇所を探すのはなかなか難しいでしょう。対処療法的にインスタンスの再起動を行うこともあるでしょうが、根本解決にはなりません。
goleakを試す
Uber社がOSSとして公開しているgoleakを利用すると簡単にゴールーチンリークを見つけることができます。
たとえば、先ほどのコードのリークを見つけるには、次のようにテスト関数の先頭にdefer goleak.VerifyNone(t)
を差し込むだけで検出できます。
func Test(t *testing.T) {
defer goleak.VerifyNone(t)
f()
}
The Go Playgroundで実行すると次のように検出されます。
=== RUN Test
10
20
prog.go:24: found unexpected goroutines:
[Goroutine 7 in state chan receive, with main.f.func1 on top of the stack:
goroutine 7 [chan receive]:
main.f.func1()
/tmp/sandbox2670982869/prog.go:13 +0x7c
created by main.f
/tmp/sandbox2670982869/prog.go:12 +0x6a
]
--- FAIL: Test (0.43s)
FAIL
Program exited.
複数のテストでチェックしたい場合は次のようにTestMain
関数でgoleak.VerifyTestMain(m)
を実行するとすべてのテストで実行されます。
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func Test(t *testing.T) {
f()
}
TestMain
関数がThe Go Playgroundでは動きませんが、ローカルで動かすと次のようになります。
$ go test
10
20
PASS
goleak: Errors on successful test run: found unexpected goroutines:
[Goroutine 19 in state chan receive, with goleak-sample.f.func1 on top of the stack:
goroutine 19 [chan receive]:
goleak-sample.f.func1()
/Users/tenntenn/repos/tenntenn/scrap/goleak-sample/a_test.go:13 +0x78
created by goleak-sample.f
/Users/tenntenn/repos/tenntenn/scrap/goleak-sample/a_test.go:12 +0x70
]
exit status 1
FAIL goleak-sample 1.263s
goleakを使いたくない場合がある場合は、次のようにフラグによって制御できるようにすると良いでしょう。
var leak bool
func init() {
flag.BoolVar(&leak, "leak", false, "use goleak")
}
func TestMain(m *testing.M) {
if leak { goleak.VerifyTestMain(m) }
os.Exit(m.Run())
}
おわりに
goleakはとても便利にゴールーチンリークを見つけることができるライブラリです。
しくみもとてもシンプルで、ゴールーチンのカウントなどは行わずに、スタックトレースをパースして行っています。
そのため、パフォーマンスに与える影響も軽微ではないかと推測できます。