Go 1.27 goroutine 泄漏剖析:用 GC 可达性找出永久阻塞的 goroutine
博客原文:https://go.dev/blog/goroutine-leak-profiles
Go 1.27 引入了 goroutine 泄漏剖析器(goroutine leak profiler),用于在运行中的 Go 程序里发现 goroutine 泄漏,包括生产系统。此前的做法依赖人工分析,这个机制是精确的,几乎不产生误报。代价是它只覆盖一部分 goroutine 泄漏:永久阻塞在 channel 或 sync 包原语上的 goroutine。这已经涵盖 goroutine 泄漏中相当大的一个子集。
该 profile 通过 runtime/pprof 包以 goroutineleak profile 类型提供,也可以通过 net/http/pprof 注册的 profile 处理程序访问。如果服务里已经接好了 net/http/pprof,不需要做任何额外配置,profile 会自动出现在 /debug/pprof/goroutineleak 端点。
采集与查看
用 curl 采集,再用 go tool pprof 查看:
$ curl http://localhost:6060/debug/pprof/goroutineleak > leak.pprof
$ go tool pprof leak.pprof
Type: goroutineleak
(pprof) list processWorkItems
Total: 116
ROUTINE ======================== main.processWorkItems.func1 in .../main.go
0 116 (flat, cum) 100% of Total
. . 31: go func() {
. . 32: res, err := processWorkItem(w)
. 116 33: ch <- result{res, err}
. . 34: }()
工作原理
Go runtime 本身已经通过垃圾回收器计算内存可达性,用的是并发三色标记清除 GC。实现泄漏检测只需要几处关键改动:
- 常规 GC 会把所有 goroutine(以及全局变量)标记为可达的 mark root。泄漏检测改为只包含未阻塞的 goroutine,因为它们必然是存活的。
- 标记阶段追踪由 mark root 传递引用的对象。
- 标记结束后,检查所有未被纳入 mark root 的阻塞 goroutine:如果某个 goroutine 至少被一个已被标记的并发原语阻塞,就把它加为 mark root,并恢复标记。
- 所有存活 goroutine 都被发现之后,任何未被加为 mark root 的 goroutine,其状态被置为 leaked。
- 最后以所有 leaked goroutine 为 mark root 再标记一次,使 GC 标记出它在常规运行中会标记的全部内存。
泄漏的 goroutine 在 traceback 输出中标记为 [leaked],而不是 [waiting] 或 [runnable]。请求 goroutineleak profile 会触发一次专门的泄漏检测 GC 周期;debug=2 会像未恢复的 panic 那样导出完整栈。
局限
如果泄漏是因为阻塞在一个仍能通过全局变量可达的原语上,或者通过某个可运行 goroutine 的局部变量可达,则不会被检测到。
建议周期性采集,例如每 4 小时一次。如果泄漏在某个时间点能被观测到,那么在执行过程中的任何后续时间点也都能被观测到,所以采集频率可以很低。
链接:https://go.dev/blog/goroutine-leak-profiles