Go 服务内存一直涨?用 pprof 定位泄漏的 4 步做法
线上 Go 服务跑着跑着内存越占越多,重启后暂时恢复,过几天又涨回来,八成是内存泄漏。Go 自带垃圾回收,泄漏一般不是对象没释放,而是对象还被某个地方引用着,GC 回收不掉。定位这类问题不用猜,用标准库自带的 pprof 按下面 4 步走,多数情况半小时能找到根因。
第一步:先确认是不是真的泄漏。登录服务器看进程 RSS 是否持续单调上涨,同时排除正常波动:刚启动时内存上涨可能是缓存预热,压测后回落不到原位也可能是 GC 惰性回收。用 top 或 ps 看 RSS 连续几天只涨不跌,再结合业务量判断:如果请求量平稳、内存却线性增长,基本可以按泄漏处理。顺手记下当前内存基线,后面修完要拿来对比。
第二步:打开 pprof 采样口。在 main 包里匿名导入 net/http/pprof,再起一个独立端口提供采样接口。代码只有两行:import _ "net/http/pprof",然后 go func() { http.ListenAndServe("localhost:6060", nil) }()。注意两点:采样口只绑 localhost,不要暴露到公网;生产环境建议再加一层鉴权或只在需要排查时临时开启,查完就关掉。
第三步:抓堆内存快照并对比。先抓一份基线:curl -s http://localhost:6060/debug/pprof/heap > heap1.out。等业务跑一段时间、内存明显上涨后再抓第二份 heap2.out。然后用 go tool pprof -base heap1.out heap2.out 对比,输入 top 看增长最多的对象类型,再用 list 加函数名看具体是哪行代码在分配。只看单份快照容易被正常缓存误导,对比两份才能看到真正“长出来”的对象。也可以加 -http 参数打开可视化页面,看火焰图更直观。
第四步:按常见泄漏写法逐个排查。第一种是 goroutine 泄漏:goroutine 阻塞在永远没人读的 channel 上,或者 select 里漏了 context 取消分支,goroutine 越积越多,连带它引用的对象也释放不掉,goroutine 数量用 pprof 的 goroutine 快照能直接看到。第二种是 time.Ticker 用完没 Stop:每次请求都 NewTicker 又不 Stop,定时器和回调会一直挂着。第三种是 HTTP 响应体没 Close:resp.Body 不关,连接和缓冲都占着,循环里调下游接口时尤其容易中招。第四种是切片或 map 只增不减:全局 map 当缓存无限追加、切片 append 后还持有底层大数组,都是典型写法问题,修法是加容量上限或定期清理。
修完之后别急着下线验证:先在测试环境压测同样流量,对比修复前后的 heap 增长曲线,确认增长斜率归零再发布。pprof 还有 allocs、mutex、block 等采样口,内存问题先用 heap,锁竞争再用 mutex,基本能覆盖日常排查。把采样口当成常备工具,内存异常先抓快照再看代码,比凭经验猜要快得多。
- 点赞
- 收藏
- 关注作者
评论(0)