编程 goroutineleak profile:在 pprof 里只看到真正泄漏的 goroutine

2026-10-24 21:03:11

Go 1.27 goroutineleak profile:在 pprof 里只看到泄漏的 goroutine

goroutine 泄漏指的是:一个 goroutine 被阻塞,而解除阻塞所需的条件永远不会满足。泄漏会持续推高内存占用和 GC 的 CPU 消耗,开了 GOMEMLIMIT 的程序尤其明显。

单测层面有 goleak,Go 1.25 的 testing/synctest 也能覆盖一部分场景,但都没法在生产环境用。线上通常看 goroutine profile,而它只能看趋势:分不清是「泄漏」还是「流量高时临时大量阻塞」,少量的泄漏可能拖几年都发现不了。

Go 1.27 引入了 goroutine leak profiler,通过 runtime/pprof 的新 profile 类型 goroutineleak 提供。已接入 net/http/pprof 的服务无需改动,端点就是 /debug/pprof/goroutineleak。它的判定精确、几乎不误报,代价是只覆盖永久阻塞在 channel 或 sync 包原语上的 goroutine。

参考:

怎么用

典型场景:processWorkItems 用无缓冲 channel 收集并发结果,某条结果带 err 时父 goroutine 提前返回,剩下的 sender 就永久卡在 `ch leak.prof
$ go tool pprof leak.prof
Type: goroutineleak
(pprof) list processWorkItems


`list` 会直接指到 `ch <- result{res, err}` 那一行,flat/cum 就是泄漏的 goroutine 数。修法是给 channel 留足容量:

```go
ch := make(chan result, len(ws))

判定方式

工具定义了一个「活性」概念:一个 goroutine 是 live 的,当且仅当它没有被并发原语阻塞,或者阻塞它的那个原语被另一个 live goroutine 引用。

实现复用了 GC 的三色标记,只改了一处:常规 GC 把所有 goroutine 当作 mark root,这里改成只有未阻塞的 goroutine 是 root。从这些显然 live 的 goroutine 出发,追踪它们能访问到的并发原语,把阻塞在这些原语上的 goroutine 也标成 live,迭代到不动点;每轮标记结束后检查被阻塞的 goroutine,如果它被已标记的原语阻塞,就加入 root 继续标记。最终没被加入的就是泄漏。最后把这些泄漏 goroutine 也加回 root 再跑一遍,让 GC 正常回收它们引用的内存。

四个真实泄漏模式

  1. 并发 worker 提前 return:上面的例子,父 goroutine 出错返回,sender 泄漏。修法是加容量为 1(或 len(ws))的缓冲。
  2. Double send:错误分支里 ch <- nil 之后漏了 return,继续执行第二条 send,接收方只收一条,发送方泄漏。修法是补上 return。
  3. select 超时:select { case <-ch: case <-ctx.Done(): },ctx 先取消,sender 没人接收就泄漏。同样给 1 容量缓冲。
  4. range channel 不 close:worker 的 for item := range ch 永不退出;另外 workers=0 时父 goroutine 的 ch <- item 也会泄漏。修法是发送完 close(ch),并对 worker 数量做校验。

局限

  • Memory overreach:并发原语如果一直被全局变量或某个可运行的 goroutine 引用,阻塞在它上面的 goroutine 不会被判为泄漏。
  • Non-standard blocking:只覆盖 channel 收发(含 nil channel)、无 default 的 select(含无 case 的 select)、以及 sync 包的 Mutex/RWMutex/WaitGroup/Cond。文件与网络 IO、直接系统调用、自定义自旋锁都不算。
  • 非确定性:只能事后发现,不能预测。
  • 性能:内存开销可忽略,但标记可能变慢。最坏情况是 daisy-chain——G0 引用的 P1 阻塞 G1,G1 引用的 P2 阻塞 G2……标记阶段实际上被串行化,而且每轮标记结束都要检查所有阻塞 goroutine,单个 GC 周期最坏 O(n²)。由于泄漏一旦出现就会一直可观测,周期性采集即可(比如每 4 小时一次),开销几乎可以忽略。
复制全文 生成海报 Go pprof 并发

推荐文章

程序员茄子在线接单