必须用?debug=2获取完整goroutine堆栈,通过go tool pprof搜索包名定位业务代码,重点检查channel操作、context未传递及连接断开时cleanup缺失。

长连接程序里 goroutine 堆栈本身不占多少内存,但堆栈背后卡住的 goroutine 会钉住大量对象,这才是内存涨的根本原因。
怎么抓到长连接场景下泄漏的 goroutine 堆栈
长连接(如 WebSocket、gRPC stream、TCP keep-alive)的特点是单个连接生命周期长,goroutine 容易被遗忘清理。默认的 /debug/pprof/goroutine 只返回摘要,必须加 ?debug=2 才能看到完整调用链:
- 执行
curl -s "http://localhost:6060/debug/pprof/goroutine?debug=2" > goroutines.txt,保存为文本文件 - 别用
?debug=1统计行数来判断——它不带栈,看不出谁在等 channel、谁在 sleep、谁在 select{} - 如果服务没开 HTTP,改用
runtime/pprof.Lookup("goroutine").WriteTo(os.Stdout, 2),第二个参数必须是2 - 生产环境慎用高频轮询,建议按需采集:压测稳定后采一次,5 分钟后再采一次,对比增长路径
堆栈里看到 runtime.selectgo 或 chan receive 怎么定位业务代码
这类状态说明 goroutine 卡在 channel 操作上,但调用栈顶层往往只显示 runtime 函数,真实泄漏点藏在闭包或中间层:
- 在 pprof Web UI(
go tool pprof -http=:8080 goroutines.txt)中,用 search 框输入你的包名,比如myapp/handler.*,快速聚焦业务代码 - 重点看是否在 for-range channel 前漏了
close(ch),或在 select 里没监听ctx.Done() - 常见陷阱:HTTP handler 启动 goroutine 处理长连接,但没把
req.Context()传进去;或者用time.AfterFunc注册回调,闭包捕获了大结构体却没清理 - 如果堆栈里有
sync.(*Map).LoadOrStore或json.Unmarshal,检查是否把连接相关对象(如*Conn、*stream)存进了全局sync.Map却没配过期逻辑
为什么 goroutine 堆栈很多,/debug/pprof/heap 却看不出对应对象
因为 goroutine 本身只占约 2KB 栈空间,真正吃内存的是它持有的对象指针——比如一个闭包捕获了含 []byte 的结构体,只要 goroutine 活着,整块底层数组就无法 GC:
- 不要只看
top输出里bytes.makeSlice占比高就下结论,那是短期分配热点,不是泄漏 - 用
go tool pprof http://localhost:6060/debug/pprof/heap?inuse_space抓快照,再结合 goroutine 堆栈里高频出现的函数名,去Focus对应类型,比如*myapp.ConnState - 若发现
inuse_space随连接数线性增长,且调用栈最终都收口到某个 handler 或 stream loop,基本可锁定是连接未关闭导致 goroutine 泄漏,进而拖住内存 - 验证方法:手动调
runtime.GC()后立刻抓 heap 快照,如果inuse_space不掉,说明对象确实被长期 goroutine 强引用着
长连接程序最易忽略的点:不是 goroutine 创建逻辑写错了,而是连接断开时没触发 cleanup 回调——比如没监听 conn.Close()、没注册 stream.Context().Done()、或 defer 语句被 panic 跳过了。pprof 堆栈只是线索,最终得回到连接生命周期管理里补全退出路径。











