不容忽视的 Goroutine Leak 以及如何排查
内容提要
本文讨论Go语言中的goroutine泄露问题:泄露指启动goroutine后不再管理,使其长期阻塞并占用内存,大量泄露后果严重。无缓冲channel会导致部分worker永久阻塞;解决办法是为channel设置buffer,并用context通知goroutine超时退出。检测方法包括etcd在TestMain中通过runtime.Stack筛选异常goroutine、uber-go/goleak库,以及运行中进程用net/http/pprof查看goroutine。核心原则是启动goroutine前须明确其如何停止。
延伸解读
无缓冲 channel 为何成为泄露高发点
文章示例中,10 个 worker 向无缓冲 result channel 发送结果,但主循环在 context 超时后退出,导致未发送成功的 worker 永久阻塞在 ch <- jobId。无缓冲 channel 要求收发双方同时就绪,一旦接收方提前离开,发送方就会卡住。这提醒我们:设计并发任务时,必须考虑接收方退出后发送方如何结束,而不是只关注正常路径。
用 buffer 与 context 双管齐下
文章给出两种互补的修复方式:为 channel 设置足够 buffer,让 worker 完成工作后能立即写入并退出;同时在 worker 内用 select 监听 ctx.Done(),超时后从发送分支返回。前者解决“没人接收”的阻塞,后者解决“任务超时”的退出。两者结合,才能覆盖示例中暴露的泄露场景,而不是只改一处。
测试阶段检测泄露的两种现成手段
etcd 在 TestMain 中通过 runtime.Stack 获取所有 goroutine 栈,过滤掉测试框架等正常 goroutine 后报告异常;uber-go/goleak 则封装了同样思路,用 defer goleak.VerifyNone(t) 在测试结束时自动检查。两者都适合在单元测试中尽早发现泄露,但需要理解其过滤逻辑,避免把正常后台 goroutine 误判为泄露。
线上排查与设计原则
对于正在运行的进程,文章建议用 net/http/pprof 访问 /pprof/goroutine,查看当前 goroutine 总数和栈信息,从而定位异常增长。这适合事后或运行中诊断,但不能替代编码时的预防。文章最后强调的核心原则是:启动 goroutine 前必须明确它如何停止。把退出条件作为设计的一部分,比事后检测更有效。
Q&A
什么是 Goroutine 泄露?为什么它会导致内存问题?
Goroutine 泄露是内存泄露的一种:你启动了一个 goroutine,但启动后就不再管理它,不关心它是否阻塞或长期存活。如果这个 goroutine 一直活着,就会占用内存资源。虽然单个 goroutine 占用内存很少,但成千上万的泄露后果会很严重。
无缓冲 channel 为什么会导致 goroutine 泄露?如何解决?
无缓冲 channel 要求发送和接收同时就绪,如果接收方不再读取,发送方 worker 就会永久阻塞在发送操作上,导致 goroutine 泄露。解决办法是为 channel 设置 buffer(如 make(chan int, 10)),让 worker 干完活后能立即发送并退出,从而释放资源。
如何用 context 通知 goroutine 超时退出以避免泄露?
在启动 goroutine 时传入 context.Context,并在 goroutine 内部用 select 同时监听 ctx.Done() 和 channel 发送操作。当 context 超时后,goroutine 会从 ctx.Done() 分支返回,从而释放资源。例如 hardJobWithCtx 函数就通过 select 处理两个 channel 来实现。
etcd 是如何在测试中检测 goroutine 泄露的?
etcd 在 TestMain 中调用 CheckLeakedGoroutine 函数。该函数通过 runtime.Stack(buffer, true) 获取所有 goroutine 的堆栈信息,然后用正则表达式过滤掉正常的 goroutine(如文件描述符关闭、testing.Main 本身等),剩下的就是异常的泄露 goroutine,并打印出来。
uber-go/goleak 库怎么用?它的原理是什么?
在测试函数中通过 defer goleak.VerifyNone(t) 即可检测泄露。测试执行完后,如果发现泄露的 goroutine,会输出其堆栈信息。其原理与 etcd 类似,也是从 runtime.Stack() 的结果中筛选出泄露的 goroutine。
如何排查正在运行的 Go 进程中的 goroutine 泄露?
可以使用 net/http/pprof,通过 curl 访问 /pprof/goroutine 端点,获取当前正在运行的 goroutine 总数和它们的堆栈信息,从而定位泄露的 goroutine。
避免 goroutine 泄露的核心原则是什么?
核心原则是:启动 goroutine 之前必须明确它如何停止。即永远不要在没有想清楚停止方式的情况下启动 goroutine。