编程 Go 服务 RSS 一直涨:pprof 抓两份 heap 用 -base 对比,定位到 gnet 的 Peek 缓存

2026-09-21 21:05:38

Go 服务 RSS 一直涨:pprof 抓两份 heap 用 -base 对比,定位到 gnet 的 Peek 缓存

参考链接:

线上现象:Go 服务 RSS 一路涨,重启后短暂恢复,过几小时继续涨。QPS 稳定甚至下降,RSS 和 Heap Objects 仍持续爬升,Full GC 后也不明显回落。

先看三组指标

不要只看某一刻的值,至少同时看:

  • process_resident_memory_bytes:进程 RSS
  • go_memstats_heap_inuse_bytes
  • go_memstats_heap_objects

判断标准:稳定负载下 HeapInuse 持续单向增长,例如每分钟涨 2MB、持续 5 分钟以上,且重启后复现。否则大概率是 GC 滞后、长生命周期缓存,或对象本来就该活久一点。

开 pprof 埋点与采样

pprof 只监听内网或 localhost,不要裸奔在公网。

import _ "net/http/pprof"

go func() {
    _ = http.ListenAndServe("127.0.0.1:6060", nil)
}()

采样至少保留两份间隔样本,单样本很难判断增长责任方:

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
# 想排除 GC 滞后可加 ?gc=1

分析时重点看几类命令:

go tool pprof -top heap_2.pb.gz
go tool pprof -base heap_1.pb.gz heap_2.pb.gz   # 只显示新增的分配
go tool pprof -http=:8081 heap_2.pb.gz          # FlameGraph

关注 inuse_space 增长最大的类型,以及同一调用路径是否持续扩大,常见对象是 mapslicestringbuffer

goroutine 也要一起看

goroutine 数量可以参考:100 正常,100-500 需关注,大于 1000 几乎 100% 是泄漏。查完整堆栈:

curl "http://localhost:6060/debug/pprof/goroutine?debug=2"

debug=1 只返回总数。重点盯 chan receiveselecttime.Sleep 状态的 goroutine。

真实案例:gnet 的 Peek 缓存

  • top20 显示 byteslice.(*Pool).Get 占 204.28MB,整个 heap 的 86.84%。
  • 沿调用链回溯:eventloop.readring.Buffer.growbyteslice.Get → 业务函数 ReadFullPacketDataFromConn,也就是每次 OnTraffic 回调的读包入口。
  • 根因:gnet 的 c.Peek(N) 会把数据拷贝进内部 c.cache,底层由 byteslice.Pool 分配;c.Next(N) 只移动读游标,不释放 cache。每次 OnTrafficPeek 分配新 cache,旧 cache 引用还在,GC 收不掉。
  • API 语义:Peek 预读,不释放 cache;Next 消费并移动游标,不释放 cache;Discard(N) 丢弃并释放,N=0 清空全部。

修复方式是在读包函数退出时 defer c.Discard(0) 强制清 cache:

defer func() {
    if _, err := c.Discard(0); err != nil {
        log.Warn().Err(err).Msg("Discard cache failed")
    }
}()

结果:总 heap 从 235.25MB 降到 30.50MB,byteslice.Pool.Get 掉出 top20

常见泄漏模式清单

  1. 全局 map 只增不删,key 持续增长。用时间戳或 UUID 当 key,却从不 delete
  2. goroutine 泄漏:channel 阻塞永不返回、context 没 cancel、重连循环旧连接没关、for+select 没 default/timeout。每个泄漏的 goroutine 会钉住一堆大对象。
  3. 缓存没有 TTL/LRU,上限失控。
  4. ticker/timer 忘 Stop
  5. channel 积压导致对象滞留。

pprof 看不见的三类

  • cgo 分配的 C 堆,例如 C.mallocC.CString,可用 valgrind 验证。
  • unsafe.Pointerreflect 绕过类型系统持有的内存。
  • netpoll、timer、worker goroutine 内部结构。

这类问题常见表现是 NumGoroutine 持续涨、RSS 涨得比 HeapInuse 快。

回归验证

同负载压测 30-60 分钟;对比修复前后 heap_objects 斜率;复抓 heap 做 -base 对比。理想状态是对象总量进入平台期,GC 后 heap_inuse 明显回落,RSS 不再单边上升。

Go 的「内存泄漏」大多不是 GC 失灵,而是对象引用路径设计失控。

复制全文 生成海报 Go pprof 内存泄漏 性能排查

推荐文章

程序员茄子在线接单