使用 Go 标准库 net/http/pprof 开启 profiling 接口,通过 go tool pprof 拉取 goroutine profile 分析栈调用链。适用场景为线上服务内存持续增长且协程数不降,风险边界是 profiling 接口需加鉴权避免暴露给公网。
先说结论:pprof 是定位 goroutine 泄漏的标准工具,重点查看阻塞在 channel 操作或锁上的协程栈。
- 先定位:确认协程数增长趋势
- 先做:抓取 goroutine profile
- 再验证:修复后观察协程数回落
命令速用版
curl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutine.debug
go tool pprof http://localhost:6060/debug/pprof/goroutine为什么会这样
goroutine 泄漏本质是协程栈内存无法被垃圾回收。协程启动后会分配栈内存,如果协程因阻塞在 channel 读写、锁竞争或无限循环中无法退出,栈内存会一直占用,导致堆内存看似飙升。
分步处理
步骤 1:引入 pprof 接口
在 main 函数导入 _ "net/http/pprof" 并启动 HTTP 服务,默认监听 6060 端口。适用场景为开发环境或已加鉴权的内网生产环境,风险边界是严禁将未鉴权的 pprof 接口暴露在公网。
步骤 2:抓取协程快照
使用 curl 命令请求 /debug/pprof/goroutine?debug=2 获取文本格式栈信息,或使用 go tool pprof 直接拉取二进制 profile。操作动作是保存文件到本地,验证结果是文件包含当前所有协程的调用栈。
步骤 3:分析阻塞点
在 pprof 交互界面输入 top 查看数量最多的协程栈,输入 list 函数名查看具体代码行。适用场景为排查具体业务逻辑,操作动作是查找 semacquire、chan send/recv 等关键字,验证结果是定位到未关闭的 channel 或未释放的锁。
怎么验证是否生效
修复代码后重启服务,观察 runtime.NumGoroutine() 返回值是否稳定。通过 Prometheus 监控 go_goroutines 指标,确认曲线不再随时间单调上升。日志中不再出现上下文超时或资源耗尽报错。
常见坑
1. 生产环境直接开启 pprof 可能导致性能抖动,建议通过环境变量控制开关。
2. debug=2 输出的文本格式适合快速查看,但二进制 profile 更适合深度分析。
3. 某些协程泄漏是间歇性的,单次快照可能抓不到,需要结合多次采样或开启连续 profile。
常见问题
pprof 会影响生产性能吗?
会有轻微开销,采集瞬间可能产生停顿。建议在内网环境使用,或限制采集频率,避免高频调用。
为什么协程数少了内存还没降?
内存回收由 GC 控制,协程退出后内存不会立即释放。需要等待下一次 GC 周期,或通过 debug.FreeOSMemory() 强制归还。
如何区分泄漏和正常波动?
观察长时间窗口内的协程数基线。如果业务低谷期协程数仍高于初始值,通常视为泄漏。
参考来源
- Go Official Blog, "Profiling Go Programs", https://go.dev/blog/pprof
- Go Package Documentation, "net/http/pprof", https://pkg.go.dev/net/http/pprof
- GitHub, "google/pprof", https://github.com/google/pprof