线上 Go 服务 RSS 一路涨,重启后短暂恢复,过几小时继续涨——这就是典型“疑似内存泄漏”场景。
别先拍脑袋改代码,先把证据链跑通:监控确认 → pprof 采样 → FlameGraph 对比 → 定位对象增长路径 → 回归验证。这套流程跑完,基本能把“玄学泄漏”打成“可复现 bug”。
一句话结论
- 先看趋势,不要只看某一刻内存值。
- 用
heap+allocs+ GC 指标一起判断,避免误判“缓存增长”。 - 线上排查优先用
go tool pprof,复杂调用链再上 FlameGraph。
一、先确认是不是“真泄漏”
先在监控看 3 组指标:
process_resident_memory_bytes(进程 RSS)go_memstats_heap_inuse_bytesgo_memstats_heap_objects
如果现象是:
- QPS 稳定甚至下降
- RSS 和 Heap Objects 仍持续爬升
- Full GC 后也不明显回落
那基本就值得进入泄漏排查。
二、给服务暴露 pprof(仅内网)
1) 标准方式接入
import _ "net/http/pprof"
func init() {
go func() {
// 只监听内网或 localhost,别裸奔到公网
_ = http.ListenAndServe("127.0.0.1:6060", nil)
}()
}
如果你用 Gin/Echo,也可以单独起一个 debug 端口,避免业务路由污染。
2) 采样命令(线上可直接跑)
# 当前堆快照
curl -s http://127.0.0.1:6060/debug/pprof/heap > heap_1.pb.gz
sleep 300
curl -s http://127.0.0.1:6060/debug/pprof/heap > heap_2.pb.gz
# 分配视角(看谁在疯狂分配)
curl -s http://127.0.0.1:6060/debug/pprof/allocs > allocs.pb.gz
关键点:至少抓两份间隔样本,单样本很难判断“增长责任方”。
三、用 pprof 快速锁定增长对象
go tool pprof -top heap_1.pb.gz
go tool pprof -top heap_2.pb.gz
go tool pprof -base heap_1.pb.gz heap_2.pb.gz
重点关注:
inuse_space增长最大的类型- 同一调用路径是否持续扩大
- 是否集中在 map/slice/string/buffer 相关对象
常见泄漏模式:
- 全局 map 只增不删(key 持续增长)
- goroutine 泄漏持有引用,导致对象无法回收
- 缓存没有 TTL/LRU,上限失控
- channel 积压导致对象滞留
四、FlameGraph 看调用链(定位“谁在养胖它”)
go tool pprof -http=:8081 heap_2.pb.gz
浏览器打开后看 FlameGraph:
- 横向越宽,内存占用越大
- 沿调用栈往下钻,找到业务函数入口
- 对比
heap_1和heap_2,看是哪条链在持续变宽
如果你看到某条 BuildXXXResponse -> append -> json.Marshal 链长期变宽,十有八九是大对象拼装 + 缓存引用没释放。
五、一个真实修复示例(可直接参考)
问题代码(简化)
var userCache = map[string]*UserProfile{}
func GetUserProfile(uid string) *UserProfile {
if v, ok := userCache[uid]; ok {
return v
}
p := loadProfile(uid)
userCache[uid] = p
return p
}
这段在高基数 UID 下会无限增长。
修复方案
- 加上限(LRU)
- 加过期(TTL)
- 加观测(缓存大小、命中率、淘汰次数)
示例(伪代码):
cache := lru.NewWithTTL(20000, 10*time.Minute)
六、回归验证:别“感觉好了”
修复后至少做这 3 件事:
- 同负载压测 30~60 分钟
- 对比修复前后
heap_objects斜率 - 复抓
heap并做-base对比
理想状态:
- 对象总量进入平台期
- GC 后
heap_inuse明显回落 - RSS 波动但不再单边上升
七、排查清单(值班时直接抄)
- pprof 端口仅内网可达
- 至少两份 heap 样本(间隔 >= 5 分钟)
- 使用
-base做增量对比 - 验证 goroutine 是否异常增长
- 验证缓存是否有上限 + 过期
- 修复后做同条件回归压测
总结
Go 的“内存泄漏”大多不是 GC 失灵,而是对象引用路径设计失控。
把证据链走完整:监控 → pprof → FlameGraph → 修复 → 回归,你就能把问题从“猜”变成“证据驱动”。这比盲改快得多,也安全得多。