编程 Go 1.27 goroutine 泄漏剖析:用 GC 可达性找出永久阻塞的 goroutine

2026-10-11 21:01:23

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。实现泄漏检测只需要几处关键改动:

  1. 常规 GC 会把所有 goroutine(以及全局变量)标记为可达的 mark root。泄漏检测改为只包含未阻塞的 goroutine,因为它们必然是存活的。
  2. 标记阶段追踪由 mark root 传递引用的对象。
  3. 标记结束后,检查所有未被纳入 mark root 的阻塞 goroutine:如果某个 goroutine 至少被一个已被标记的并发原语阻塞,就把它加为 mark root,并恢复标记。
  4. 所有存活 goroutine 都被发现之后,任何未被加为 mark root 的 goroutine,其状态被置为 leaked。
  5. 最后以所有 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

复制全文 生成海报 Go goroutine pprof 性能 运行时

推荐文章

程序员茄子在线接单