17
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

tenntennAdvent Calendar 2022

Day 9

とっても便利なgoleakでゴールーチンリークを見つけよう

Posted at

はじめに

どうもナレッジワークtenntennです。

本記事では、テストでゴールーチンリークを検出する便利なライブラリについて紹介し、その仕組について解説します。

ゴールーチンリークとは

ゴールーチンリークとは、ゴールーチンが終了せず、ずっと残ってしまうことを指します。ゴールーチンが終了しないと、メモリも開放されないため、メモリリークが発生します。

たとえば、次のようなコードではゴールーチンリークが発生します。

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
}

The Go Playgroundで動かす

関数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で動かす

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はとても便利にゴールーチンリークを見つけることができるライブラリです。
しくみもとてもシンプルで、ゴールーチンのカウントなどは行わずに、スタックトレースをパースして行っています。
そのため、パフォーマンスに与える影響も軽微ではないかと推測できます。

17
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?