Comment le langage Go détecte les fuites de goroutine dans les tests

Lien d'origine : Comment le langage Go détecte les fuites de goroutine lors des tests

avant-propos

Bonjour à tous, je suis asong;

Comme nous le savons tous, gorourtinela conception du langage est l'élément central de la mise en œuvre simultanée du langage. Il est facile à utiliser, mais il fuite est l'une des maladies gravesGorencontre également diverses maladies incurables. Parmi elles, la Bien sûr, il est là. Il est open source par l' équipe et peut être utilisé pour détecter des fuites. Il peut être combiné avec des tests unitaires pour éviter que cela ne se produise. Jetons un coup d'œil dans cet article .goroutinepprofgoleakUbergoroutinegoleak

fuite de routine

Je ne sais pas si vous avez rencontré des goroutinefuites dans votre développement quotidien. goroutineLes fuites sont en fait goroutinebloquantes. Ces fuites bloquées goroutinesurvivront jusqu'à la fin du processus, et la mémoire de la pile qu'elles occupent ne peut pas être libérée, ce qui entraîne de moins en moins de mémoire disponible dans le système jusqu'à ce qu'il plante ! Un bref résumé de plusieurs causes de fuite courantes :

  • GoroutineLa logique interne entre dans une boucle infinie et continue d'occuper des ressources
  • GoroutineLorsqu'accouplé channel/ mutexutilisé, il a été bloqué en raison d'une mauvaise utilisation
  • GoroutineLa logique à l'intérieur attend longtemps, faisant Goroutineexploser le nombre

Ensuite, nous utilisons la combinaison classique de Goroutine+ pour montrer la fuite ;channelgoroutine

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

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

Cet exemple channeloublie d'initialiser, et les opérations de lecture et d'écriture provoqueront un blocage. Si cette méthode consiste à écrire un seul test, elle ne pourra pas détecter le problème :

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

résultat de l'opération :

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

Le test intégré ne peut pas être satisfait, alors introduisons goleak-le pour le tester.

objectifs

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的实现原理:

Utilisez la runtime.Stack()méthode pour obtenir toutes les informations de la pile en cours d'exécution goroutine. Par défaut, les éléments de filtre qui n'ont pas besoin d'être détectés sont définis par défaut. Le nombre de détections + intervalle de détection est défini par défaut, et la détection est effectuée périodiquement. Enfin , après de multiples vérifications, les autres ne sont pas retrouvées, et goroutineon juge qu'il n'y a pas de goroutinefuite . .

Résumer

Dans cet article, nous partageons un outil qui peut trouver des goroutinefuites dans les tests, mais il a encore besoin d'une prise en charge complète des cas de test, ce qui expose l'importance des cas de test. Amis, de bons outils peuvent nous aider à trouver des problèmes plus rapidement, mais le code La qualité est toujours là nos propres mains, allez, les garçons ~.

Eh bien, cet article se termine ici, je suis en train de chanter , à la prochaine fois.

Bienvenue sur le compte public : Golang Dream Factory

Références

Je suppose que tu aimes

Origine juejin.im/post/7098353322507108388
conseillé
Classement