Go程泄漏怎么通过pprof定位
摘要:# Go协程泄漏,别慌!用pprof“抓鬼”实录 那天下午,我正喝着咖啡,突然收到告警——服务内存占用像坐了火箭,半小时涨了2个G。心里咯噔一下:坏了,八成是协程泄漏了。 这种场景你应该不陌生吧?Go程序跑着跑着,内存越来越高,CPU也不正常,但业务量…
Go协程泄漏,别慌!用pprof“抓鬼”实录
那天下午,我正喝着咖啡,突然收到告警——服务内存占用像坐了火箭,半小时涨了2个G。心里咯噔一下:坏了,八成是协程泄漏了。
这种场景你应该不陌生吧?Go程序跑着跑着,内存越来越高,CPU也不正常,但业务量明明没变化。很多团队的第一反应是“加机器”、“重启服务”——这能临时解决问题,但根本不知道“鬼”在哪。
今天我就跟你聊聊,怎么用Go自带的pprof工具,把协程泄漏这个“鬼”给揪出来。
一、协程泄漏到底有多可怕?
先说句大实话:Go的goroutine太容易创建了,以至于泄漏成了家常便饭。
你开个for循环,里面go func()起个协程,如果没控制好退出条件,或者channel没close干净,协程就会一直挂着。一个两个没事,但要是每秒泄漏几十个……几天后你的服务就成内存怪兽了。
我见过最离谱的案例:一个简单的HTTP服务,因为一个第三方库的bug,每处理一个请求就泄漏3个协程。上线一周后,8G内存的机器被吃满,服务直接OOM(内存溢出)崩溃。
这类低配问题真扛不住,别硬撑。
二、pprof不是摆设,是你的“CT机”
很多人知道pprof,但觉得“配置麻烦”、“看不懂图”。其实吧,它用起来比你想的简单。
1. 先把“监控探头”装上
在你的main.go里加几行代码:
import _ "net/http/pprof"
func main() {
// 在另一个端口开pprof服务
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// ...你的业务代码
}
这就行了。现在访问 http://localhost:6060/debug/pprof/ 就能看到各种性能数据。
2. 协程泄漏怎么看?
重点看两个地方:
goroutine:当前存活的协程数heap:内存分配情况
如果协程数只增不减,内存占用持续上涨——基本可以确定有泄漏。
三、实战:一步步“抓鬼”
上周我帮朋友排查的一个真实案例,特别典型。
他们的订单服务,平时稳定在3000个协程左右。突然有一天,协程数开始缓慢增长,24小时后到了5万+,服务响应慢得像蜗牛。
第一步:先拍个“快照”
# 抓取当前所有协程的堆栈信息
go tool pprof http://localhost:6060/debug/pprof/goroutine
# 生成可视化图(需要graphviz)
go tool pprof -png http://localhost:6060/debug/pprof/goroutine > goroutine.png
第二步:看“热点”在哪
生成的图里,你会看到一堆方框和箭头。重点看那些“孤岛”——只有进没有出的协程。
在这个案例里,我发现了一个可疑的调用链:
main.processOrder -> thirdparty.SendAsync -> (卡在channel发送)
有2000多个协程卡在同一个channel的发送操作上,发送完就没下文了。
第三步:定位代码
用pprof的list命令看具体代码:
(pprof) list thirdparty.SendAsync
结果发现是这么个坑:
func SendAsync(data []byte) {
ch := make(chan bool) // 问题在这!
go func() {
// 模拟发送
time.Sleep(100 * time.Millisecond)
ch <- true
}()
<-ch // 等待完成
}
问题来了:每次调用都创建新channel,起新协程,但万一协程里的发送失败了(比如panic),主协程就会永远卡在<-ch这里。
协程泄漏的经典模式:生产者-消费者模型里,一边挂了另一边还在等。
第四步:修复和验证
修复其实很简单:
func SendAsync(data []byte) {
ch := make(chan bool, 1) // 加个缓冲
go func() {
defer func() {
ch <- true // 无论如何都发送
}()
// 业务逻辑
}()
<-ch
}
或者用context.WithTimeout加个超时。
修复后重新部署,观察pprof的协程数——稳定了,不再增长。问题解决。
四、这些“坑”你八成也踩过
根据我的经验,协程泄漏最常见的是这几种:
- channel没关干净:就像上面的例子,发送/接收卡死了
- for循环里的go func:忘了加退出条件,或者条件永远不满足
- context没传递超时:下游服务挂了,上游一直等
- sync.WaitGroup用错:Add和Done没成对出现
- 第三方库的bug:这个最隐蔽,得靠pprof才能发现
有个小技巧:在测试环境故意压测,然后用pprof看协程增长趋势。提前发现问题,比线上炸了再修强一百倍。
五、pprof的“高级玩法”
如果你觉得命令行不够直观,试试这些:
1. 火焰图看协程
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine
浏览器打开localhost:8080,选“Flame Graph”——哪里协程多,哪里就“火”旺,一目了然。
2. 对比分析
怀疑某个版本引入泄漏?抓两个时间点的profile对比:
# 先抓一个
curl -s http://localhost:6060/debug/pprof/goroutine > base.prof
# 过段时间再抓一个
curl -s http://localhost:6060/debug/pprof/goroutine > current.prof
# 对比
go tool pprof -base base.prof http://localhost:6060/debug/pprof/goroutine
新增的协程调用栈会高亮显示,泄漏点藏不住。
3. 持续监控
在生产环境,你可以定期采集pprof数据,用Prometheus+Grafana画成趋势图。看到协程数突然“跳涨”,立马报警——在用户发现之前,你就知道有问题了。
六、最后说几句心里话
我见过太多团队,遇到性能问题就盲目“优化”:加缓存、改算法、换机器……其实很多时候,问题就出在几行简单的并发代码上。
pprof这东西,刚开始用可能觉得复杂,但用顺手了就像医生的听诊器。不需要等病人(服务)快死了才用,平时体检就该常看看。
如果你的Go服务还没接pprof——别等了,今天就加上吧。真等到内存爆了、服务挂了再查,那成本可就高了去了。
行了,不废话了。下次再遇到协程泄漏,你知道该怎么做了吧?

