Go言語がテストでゴルーチンリークを検出する方法

元のリンク:Go言語がテストでゴルーチンリークを検出する方法

序文

みなさん、こんにちはasong

ご存知のように、言語gorourtineのデザインは言語の同時実装のコアコンポーネントであり、使いやすいだけでなく、漏出は深刻な病気の1つですGoさまざまな難病に遭遇します。その中でも、もちろん、彼はここにいます。チームリークを検出するために使用できます。ユニットテストと組み合わせて、リークの発生を防ぐことができます。この記事を見てみましょうgoroutinepprofgoleakUbergoroutinegoleak

ゴルーチンリーク

goroutine毎日の開発でリークが発生したかどうかはわかりません。goroutineリークは実際にgoroutineブロックされています。これらのブロックされたgoroutineメモリはプロセスが終了するまで存続し、占有しているスタックメモリを解放できないため、使用可能なメモリがますます少なくなります。システムがクラッシュするまで!いくつかの一般的なリークの原因の簡単な要約:

  • Goroutine内部ロジックは無限ループに入り、リソースを占有し続けます
  • Goroutine嵌合channel/mutex使用時、不適切な使用によりブロックされています
  • Goroutine内部のロジックは長時間待機し、Goroutine数が爆発的に増加します

次に、 Goroutine+channelの古典的な組み合わせを使用してgoroutineリークを示します。

func GetData() {
	var ch chan struct{}
	go func() {
		<- ch
	}()
}

func main()  {
	defer func() {
		fmt.Println("goroutines: ", runtime.NumGoroutine())
	}()
	GetData()
	time.Sleep(2 * time.Second)
}
复制代码

この例ではchannel初期化を忘れており、読み取り操作と書き込み操作の両方でブロッキングが発生します。このメソッドが単一のテストを書き込む場合、問題を検出できません。

func TestGetData(t *testing.T) {
	GetData()
}
复制代码

演算結果:

=== RUN   TestGetData
--- PASS: TestGetData (0.00s)
PASS
复制代码

内蔵のテストでは満足できないので、紹介goleakしてテストしてみましょう。

目標

github地址github.com/uber-go/gol…

使用goleak主要关注两个方法即可:VerifyNoneVerifyTestMainVerifyNone用于单一测试用例中测试,VerifyTestMain可以在TestMain中添加,可以减少对测试代码的入侵,举例如下:

使用VerifyNone:

func TestGetDataWithGoleak(t *testing.T) {
	defer goleak.VerifyNone(t)
	GetData()
}
复制代码

运行结果:

=== RUN   TestGetDataWithGoleak
    leaks.go:78: found unexpected goroutines:
        [Goroutine 35 in state chan receive (nil chan), with asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1 on top of the stack:
        goroutine 35 [chan receive (nil chan)]:
        asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1()
        	/Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:12 +0x1f
        created by asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData
        	/Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:11 +0x3c
        ]
--- FAIL: TestGetDataWithGoleak (0.45s)

FAIL

Process finished with the exit code 1
复制代码

通过运行结果看到具体发生goroutine泄漏的具体代码段;使用VerifyNone会对我们的测试代码有入侵,可以采用VerifyTestMain方法可以更快的集成到测试中:

func TestMain(m *testing.M) {
	goleak.VerifyTestMain(m)
}
复制代码

运行结果:

=== RUN   TestGetData
--- PASS: TestGetData (0.00s)
PASS
goleak: Errors on successful test run: found unexpected goroutines:
[Goroutine 5 in state chan receive (nil chan), with asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1 on top of the stack:
goroutine 5 [chan receive (nil chan)]:
asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData.func1()
	/Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:12 +0x1f
created by asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector.GetData
	/Users/go/src/asong.cloud/Golang_Dream/code_demo/goroutine_oos_detector/main.go:11 +0x3c
]

Process finished with the exit code 1
复制代码

VerifyTestMain的运行结果与VerifyNone有一点不同,VerifyTestMain会先报告测试用例执行结果,然后报告泄漏分析,如果测试的用例中有多个goroutine泄漏,无法精确定位到发生泄漏的具体test,需要使用如下脚本进一步分析:

# 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 -e "\n$test failed"; done
复制代码

这样会打印出具体哪个测试用例失败。

goleak实现原理

VerifyNone入口,我们查看源代码,其调用了Find方法:

// Find looks for extra goroutines, and returns a descriptive error if
// any are found.
func Find(options ...Option) error {
  // 获取当前goroutine的ID
	cur := stack.Current().ID()

	opts := buildOpts(options...)
	var stacks []stack.Stack
	retry := true
	for i := 0; retry; i++ {
    // 过滤无用的goroutine
		stacks = filterStacks(stack.All(), cur, opts)

		if len(stacks) == 0 {
			return nil
		}
		retry = opts.retry(i)
	}

	return fmt.Errorf("found unexpected goroutines:\n%s", stacks)
}
复制代码

我们在看一下filterStacks方法:

// filterStacks will filter any stacks excluded by the given opts.
// filterStacks modifies the passed in stacks slice.
func filterStacks(stacks []stack.Stack, skipID int, opts *opts) []stack.Stack {
	filtered := stacks[:0]
	for _, stack := range stacks {
		// Always skip the running goroutine.
		if stack.ID() == skipID {
			continue
		}
		// Run any default or user-specified filters.
		if opts.filter(stack) {
			continue
		}
		filtered = append(filtered, stack)
	}
	return filtered
}
复制代码

这里主要是过滤掉一些不参与检测的goroutine stack,如果没有自定义filters,则使用默认的filters

func buildOpts(options ...Option) *opts {
	opts := &opts{
		maxRetries: _defaultRetries,
		maxSleep:   100 * time.Millisecond,
	}
	opts.filters = append(opts.filters,
		isTestStack,
		isSyscallStack,
		isStdLibStack,
		isTraceStack,
	)
	for _, option := range options {
		option.apply(opts)
	}
	return opts
}
复制代码

从这里可以看出,默认检测20次,每次默认间隔100ms;添加默认filters;

总结一下goleak的实现原理:

この方法を使用して、runtime.Stack()現在実行中のすべてのスタック情報を取得しますgoroutine。デフォルトでは、検出する必要のないフィルター項目がデフォルトで定義されています。デフォルトでは、検出数+検出間隔が定義され、定期的に検出が実行されます。 、複数回チェックしたところ、残りは見つからず、漏れgoroutineはないと判断しましgoroutineた。

要約する

この記事ではgoroutine、テストのリークを見つけることができるツールを共有しますが、それでもテストケースの重要性を明らかにする完全なテストケースのサポートが必要です。友だち、優れたツールは問題をより早く見つけるのに役立ちますが、コードの品質はまだ残っています私たち自身の手、さあ、男の子〜。

さて、この記事はここで終わります、私は同意します、次回お会いしましょう。

パブリックアカウントへようこそ:Golang Dream Factory

参考文献

おすすめ

転載: juejin.im/post/7098353322507108388