通过 net/http/pprof 接口采集 goroutine 和 heap 快照,对比不同时间点的堆栈差异,定位未退出的协程代码行。适用于 Go 服务内存持续上涨场景,生产环境开启需注意采样开销。
先说结论:pprof 是定位 Goroutine 泄露的标准工具,核心是对比两个时间点的堆栈快照。
- 先定位:确认内存上涨伴随 goroutine 数量持续增加。
- 先做:采集泄露前后的 pprof 文件并进行 diff 分析。
- 再验证:修复代码后观察 goroutine 数量是否回落稳定。
命令速用版
以下命令用于快速采集和分析 goroutine profile,假设服务暴露了 6060 端口。
# 采集当前 goroutine 堆栈信息
curl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutine_before.txt
# 等待一段时间或复现问题后再次采集
curl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutine_after.txt
# 使用 go tool pprof 进行对比分析
go tool pprof -base goroutine_before.txt goroutine_after.txt为什么会这样
Goroutine 泄露会导致内存上涨,因为每个协程都占用独立的栈内存空间。Go 运行时不会自动回收未退出的协程,即使协程处于阻塞状态,其栈内存依然被保留。当泄露的协程数量随时间累积,堆内存中的栈空间分配随之增加,表现为 RSS 或 HeapAlloc 指标持续上升。
分步处理
按以下步骤操作,确保每一步都有明确的检查点。
1. 引入 pprof 支持
在代码中导入 net/http/pprof 包,确保 HTTP 服务启动后自动注册 debug 接口。
import _ "net/http/pprof"2. 确认泄露现象
监控面板中观察 goroutine 数量曲线。如果内存上涨的同时 goroutine 数量呈阶梯状或线性增长,基本确认为协程泄露。
3. 采集快照
在内存较低时采集一次 baseline,在内存上涨明显时采集一次 snapshot。使用 debug=2 参数可获取文本格式堆栈,方便直接查看。
4. 分析差异
使用 go tool pprof 的 -base 参数对比两个文件。输出结果中显示正数的行代表新增的协程堆栈,重点关注业务代码行的调用次数。
5. 定位代码
查看堆栈跟踪中阻塞的位置,常见于 channel 读写、锁等待、select 未超时等场景。检查对应代码是否缺少 context 取消或超时控制。
怎么验证是否生效
修复代码后,重新部署服务并观察监控指标。验证标准是 goroutine 数量在业务低谷期能回落到基准线,且内存增长曲线变平。使用 pprof 再次采集快照,确认之前泄露的堆栈条目不再新增。
常见坑
1. 运行时协程干扰
pprof 输出中包含 GC、调度器等运行时协程,分析时需过滤掉 runtime 包下的堆栈,专注业务包路径。
2. 生产环境开销
开启 pprof 会引入少量 CPU 和内存开销,高并发场景下建议通过防火墙限制 debug 接口访问,或仅在排查时临时开启。
3. 采样率误解
block 和 mutex profile 默认关闭,需要手动设置 SetBlockProfileRate 开启,否则可能漏掉因锁竞争导致的协程阻塞。
常见问题
pprof 会影响生产服务性能吗?
会有轻微影响,但通常可接受。CPU profile 采样开销稍大,goroutine 和 heap profile 开销较小,建议限制访问来源。
如何在不重启服务的情况下开启 pprof?
如果代码未预埋 pprof 入口,无法动态开启。必须在代码中导入 net/http/pprof 并重新编译部署。
goroutine 数量多少算泄露?
没有固定阈值,关键看趋势。如果业务请求量稳定但 goroutine 数量持续只增不减,即可判定为泄露。
参考来源
- Go Official Blog, "Profiling Go Programs", https://go.dev/blog/pprof
- Go Package Documentation, "net/http/pprof", https://pkg.go.dev/net/http/pprof