元のリンク:Go言語がテストでゴルーチンリークを検出する方法
序文
みなさん、こんにちは
asong
。ご存知のように、言語
gorourtine
のデザインは言語の同時実装のコアコンポーネントであり、使いやすいだけでなく、漏出は深刻な病気の1つですGo
さまざまな難病に遭遇します。その中でも、もちろん、彼はここにいます。チームリークを検出するために使用できます。ユニットテストと組み合わせて、リークの発生を防ぐことができます。この記事を見てみましょう。goroutine
pprof
goleak
Uber
goroutine
goleak
ゴルーチンリーク
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
主要关注两个方法即可:VerifyNone
、VerifyTestMain
,VerifyNone
用于单一测试用例中测试,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
参考文献