pprof 可抓取运行中长连接程序的 Goroutine 堆栈定位泄漏:用 /debug/pprof/goroutine?debug=2 获取完整栈,配合 grep 筛选 net.Conn/Read/Write/select;需手动注册 net/http/pprof 并独立端口;注意 runtime.gopark 是正常阻塞态,关键看上层调用;泄漏高发于 defer 未执行、IdleConnTimeout 失效、context.Cancel 重复调用及断连后 channel/timer 傻等。

如何用 pprof 抓取长连接程序的 Goroutine 堆栈
Go 程序跑着跑着 Goroutine 数飙升,但服务没明显报错?别急着重启,pprof 能直接看到“谁在死守连接、谁忘了 close、谁卡在 select 里”。关键是:必须抓运行中真实状态,不能只看启动快照。
默认 /debug/pprof/goroutine?debug=2 返回的是所有 Goroutine 的完整堆栈(含已阻塞、休眠、运行中),但长连接服务常有成百上千个 idle 连接 Goroutine,全打出来眼花缭乱。建议加 ?pprof_no_headers=1 避免 HTTP 头干扰,再用 grep -A 5 -B 5 快速定位可疑模式:
curl 'http://localhost:6060/debug/pprof/goroutine?debug=2&pprof_no_headers=1' | grep -A 5 -B 5 "net.Conn\|Read\|Write\|select"
-
?debug=1只返回摘要(Goroutine 总数 + 每种状态计数),适合快速判断是否异常膨胀 -
?debug=2才返回每个 Goroutine 的完整调用栈,必须用这个定位具体代码行 - 如果程序没开
net/http/pprof,得手动注册:import _ "net/http/pprof"并起一个独立http.Server,别和业务端口混用,避免被外部访问
为什么 goroutine 堆栈里总看到 runtime.gopark 和 net.(*conn).Read
这不是 bug,是 Go 网络 I/O 的正常阻塞态表现。关键要看它前面几层调用——真正的问题藏在上层逻辑里。
典型危险信号:
- 堆栈顶部是
runtime.gopark,中间有net.(*conn).Read,但再往上不是你的 handler 函数,而是某个自定义bufio.Reader或封装的readLoop——说明读逻辑没做超时控制 - 堆栈里反复出现
select { case 但没有 <code>time.After或context.WithTimeout——这种“伪非阻塞”会持续占 Goroutine - 同一段代码(比如
handleConn)出现在几百个 Goroutine 堆栈里,且都卡在conn.Read或conn.Write——基本可断定连接没被正确回收或超时关闭
用 go tool pprof 分析 goroutine dump 文件的实操要点
线上环境不能一直开着 /debug/pprof,更不能让 curl 直接拉几 MB 堆栈文本。稳妥做法是定时 dump 到文件,再离线分析。
抓取命令(带时间戳,防覆盖):
curl -s "http://localhost:6060/debug/pprof/goroutine?debug=2" > goroutine-$(date +%s).txt
分析时别用默认的交互式 top,长连接程序的 Goroutine 堆栈深度浅、数量大,top 显示的“flat”值意义不大。改用:
-
go tool pprof -http=:8080 goroutine-171xxxx.txt→ 打开 Web 界面后点 “Top” 标签,选focus输入你怀疑的函数名(如handleConn),看它被多少 Goroutine 调用 - 导出火焰图:
go tool pprof --svg goroutine-171xxxx.txt > goroutine.svg,重点观察宽而平的底部节点——那些就是大量 Goroutine 共同阻塞的位置 - 注意:goroutine profile 是采样快照,不是 CPU profile,
-seconds参数无效;它不测耗时,只记“此刻存在”
长连接场景下 goroutine 泄漏的三个高发盲区
很多泄漏不是代码写错,而是对 Go 运行时行为理解偏差导致的。
-
defer conn.Close()在 handler 函数退出时才触发,但如果 handler 因 panic 中断,且没 recover,defer不执行 → 连接永远不关。务必在handleConn入口加defer func(){if r:=recover();r!=nil{conn.Close()}}() -
http.Server的IdleConnTimeout对长连接无效——它只管客户端复用的空闲连接;服务端自己维护的长连接(如 WebSocket、TCP 自定义协议)必须自己实现心跳+超时 - 用
context.WithCancel控制 Goroutine 生命周期时,如果 cancel 函数被多个 goroutine 同时调用,Go 允许,但不会报错;实际效果是第一次调用生效,后续静默失败——检查 cancel 是否被重复触发
最麻烦的泄漏往往发生在连接断开后,Goroutine 还在等一个永远不会到达的 channel 消息,或者在一个没设 timeout 的 time.Timer 上傻等。这时候 goroutine?debug=2 里看不到业务函数名,只有一串 runtime 调用,就得顺着 channel 变量名或 timer 名反向追踪代码。


















