子兮子兮 子兮子兮

子兮子兮风兮寒,三江七泽情洄沿。

目录
一次 goroutine 泄露的排查过程
/      

一次 goroutine 泄露的排查过程

goroutine 泄露并不会立刻杀死服务,它更像慢性病,直到某一天系统突然“扛不住了”。

这是一次发生在生产环境的真实问题排查过程,记录下来,一方面是给自己留档,另一方面也希望能给后来遇到类似问题的人一点参考。


一、问题的出现:服务越来越“沉”

最初的问题并不明显。

  • 服务没有 panic;
  • CPU 使用率正常;
  • QPS 没有明显波动。

唯一异常的是:

  • 内存缓慢上涨
  • 重启服务后恢复正常
  • 运行时间越长,响应时间(RT)越不稳定

发生这类现象,经验上第一反应就是:存在资源泄露问题。


二、确认:是不是 goroutine 泄露?

在 Go 服务中,最容易被忽略的泄露就是 goroutine。

快速确认手段

通过 /debug/pprof/goroutine 查看 goroutine 数量:

curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=1

观察到一个关键现象:

  • goroutine 数量只增不减
  • 高峰期结束后也不会回落。

这基本可以确定:goroutine 没有正常退出


三、定位方向:哪些 goroutine 没退出?

接下来不是看数量,而是看它们在“干什么”

使用 pprof 分析调用栈

go tool pprof http://127.0.0.1:6060/debug/pprof/goroutine

在调用栈中,发现大量 goroutine 卡在:

chan receive

并且调用路径高度一致。

这说明问题不是偶发,而是某一类逻辑在稳定地产生泄露


四、问题代码:一个“看起来很正常”的协程

最终定位到如下代码(已简化):

func asyncNotify(data Data) {
	ch := make(chan Result)

	go func() {
		result := doRequest(data)
		ch <- result
	}()

	select {
	case <-time.After(1 * time.Second):
		return
	case res := <-ch:
		handle(res)
	}
}

表面逻辑:

  • 启动 goroutine 异步请求;
  • 最多等 1 秒;
  • 超时就返回。

五、真正的问题:谁在等谁?

关键点在这里:

ch := make(chan Result) // 无缓冲 channel

当发生超时时:

  • select 直接 return,但 goroutine 仍在执行
  • 一旦 doRequest 返回结果,尝试 ch <- result此时已经没有接收者

结果:

goroutine 永远阻塞在发送 channel 的那一行。

每一次超时,都会留下一个“活着但无法退出”的 goroutine。


六、为什么这个问题很隐蔽?

这个问题在测试环境几乎无法复现,原因包括:

  • 测试数据少;
  • 网络稳定;
  • 几乎不超时。

而在生产环境:

  • 网络抖动;
  • 下游不稳定;
  • 请求量大。

超时路径被频繁触发,泄露才逐渐累积。


七、修复方案:让 goroutine 有“退路”

修复方式一:带缓冲的 channel

ch := make(chan Result, 1)

这样即使外层不再接收,发送也不会阻塞。

修复方式二:使用 context 控制生命周期(推荐)

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

ch := make(chan Result, 1)

go func(ctx context.Context) {
	select {
	case <-ctx.Done():
		return
	default:
		result := doRequest(data)
		ch <- result
	}
}(ctx)

关键思想:

  • goroutine 必须能感知“我已经不需要你了”;
  • channel 只是通信手段,不是生命周期管理工具。

八、事后总结:goroutine 泄露的典型信号

这次事故之后,我们总结了几条可操作的经验

  1. 只要有 goroutine,就必须有退出路径;
  2. select + timeout 一定要检查是否会留下阻塞发送;
  3. 默认使用 带缓冲 channel
  4. 后台任务必须绑定 context
  5. goroutine 数量必须纳入监控。

九、写在最后

goroutine 泄露不会立刻让系统崩溃,但它会在你最没有防备的时候,悄悄掏空系统的稳定性

如果你在生产环境看到:

  • 内存缓慢上涨;
  • RT 不稳定;
  • goroutine 数量异常。

请一定第一时间想到它。