【译文】原文地址
使用APM监控goroutine的数量,可以容易的发现goroutine泄漏。下图是来自NewRelic(APM工具开发商)的一张监控goroutines的图片。
协程泄漏会导致协程数量持续增长直到服务器奔溃。然而,有很多方法来避免泄漏即使在代码部署之前也可以。
探测泄漏
Uber的Go团队在Github上面是一个很活跃的团队,开发了一个Goroutine泄漏探测器,是一个用于和单元测试集成的工具。这个包实际上是监测单元测试代码运行时协程的泄漏。以下是一个函数存在goroutine泄漏:
func leak() error {
go func() {
time.Sleep(time.Minute)
}()
return nil
}
下面的代码是该函数的测试代码:
func TestLeakFunction(t *testing.T) {
defer goleak.VerifyNone(t)
if err := leak(); err != nil{
t.Fatal("error not expected")
}
}
运行测试代码会发现协程泄漏:
=== RUN TestLeakFunction
leaks.go:78: found unexpected goroutines:
[Goroutine 19 in state sleep, with time.Sleep on top of the stack:
goroutine 19 [sleep]:
上面的错误提示了两个错误信息:
- 泄漏协程栈顶,状态信息。这个信息有助于快速发现是哪个协程泄漏。
-
泄漏协程的ID,有助于跟踪器来可视化协程。可以使用go test ./... -trace trace.out
从跟踪的信息,可以查看goroutine的详细执行信息。
泄漏的携程被监测到后,而且还有关于泄漏的信息。现在我们明白它是如何工作的,因此需注意这个监测的缺陷。
内部
要能监测泄露,唯一的要求就是在测试代码最后面调用该库来检查泄漏的携程即可。实际上,它会监测所有goroutine而不仅仅是泄漏的goroutine。
首先检测器列出所有创建的goroutine。以下就是上面的列子所有goroutine列表:
Goroutine的栈信息来源于导出函数runtime.Stack(go标准库函数)。因此,任何人都可以拿到这些信息。
然后,从这个协程列表中,泄漏检测器就可以通过对这些信息进行分析,然后将属于标准库的协程删除。如下:
- test包创建的goroutine用来运行测试用例的-前面例子中第二个goroutine。
- runtime创建的goroutine,比如接收信号的协程。
- 当前协程-上面例子中的第一个协程。
最后,一旦过滤掉所有正常的协程,如果没有剩余的其他协程,意味着不存在协程泄漏。但我们发现这里面存在一些限制: - 第三方库或内部协程启动的后台goroutine,并没有很好的处理,可能会产生错误的报告。
- 如果其他的测试用例中没有使用泄漏检测。如果再次运行,协程还存在就会被监测到,将会误报。
这个工具不是完美的,但是了解其中的可能性和限制有助于通过测试来检测泄漏,避免对已经部署到生产环境中的代码进行调试。
有趣的是这个方法被广泛用在了net/http包来检测泄漏协程。如下是一个例子:
再次看到,内部函数afterTest查看goroutine栈来检测存在的泄漏。