这是典型“非 go 堆内存泄漏”信号——rss 持续上涨而 runtime.readmemstats 无明显变化,主因是 malloc 分配的 c 堆、cgo 资源未释放或 mmap 未回收,需用 pprof 结合 cgroup 和 flame graph 定位 cgo 分配热点。

Go 应用容器里 RSS 持续上涨但 runtime.ReadMemStats 没明显变化?别急着杀进程
这是典型“非 Go 堆内存泄漏”信号——malloc 分配的 C 堆、CGO 调用未释放的资源、或 mmap 映射未回收,都逃不开 runtime.ReadMemStats 的统计范围。容器监控看到 RSS 从 200MB 涨到 1.2GB,而 Go 堆只占 80MB,大概率是这类问题。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 先用
cat /sys/fs/cgroup/memory/memory.usage_in_bytes确认容器 RSS(不是ps aux那个),排除宿主机其他进程干扰 - 跑
go tool pprof http://localhost:6060/debug/pprof/heap,看 top 输出里有没有大量runtime.mmap或C.malloc调用栈 - 如果用了
database/sql+ CGO 驱动(比如github.com/mattn/go-sqlite3),检查是否漏调rows.Close()—— SQLite 的sqlite3_prepare_v2会直接 mmap 内存,不 close 就不释放 - 禁用
GODEBUG=madvdontneed=1(默认开启)可让 runtime 更积极归还内存给 OS,但治标不治本;真泄漏得追源头
用 pprof 抓住 CGO 分配热点,但别只看 top
pprof 默认聚合的是 Go 调用栈,CGO 进入 C 层后就断了。你看到 top 里全是 runtime.goexit,不代表没泄漏——只是栈没透出来。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 启动时加环境变量
GODEBUG=cgocheck=2,强制校验 CGO 指针生命周期,能提前暴露野指针或提前释放 - 用
go tool pprof -http=:8080 binary http://localhost:6060/debug/pprof/heap,进 Web 界面后点 “View” → “Flame Graph”,再右上角选 “Focus on” 输入mmap或malloc,直接过滤出可疑 C 分配路径 - 如果用
net/http处理大文件上传,确认没用bytes.Buffer无限制拼接:它底层用make([]byte, 0, n),n 过大时触发 mmap,且扩容策略会让 RSS 滞后释放
自动报警不能只看 RSS 阈值,要结合增长率 + 容器生命周期
一个刚启动 5 分钟的 Go 服务 RSS 到 400MB 可能正常(加载模板、缓存预热);但运行 2 小时后每分钟涨 15MB,就是危险信号。单纯设 “RSS > 1GB 报警” 会产生大量误报。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 采集周期用 15s 间隔(Kubernetes
cAdvisor默认 10s),计算滑动窗口内 RSS 的每分钟增量(单位 MB/min),阈值设为 5MB/min 持续 3 个周期即告警 - 在 Prometheus 中写 rule:
rate(container_memory_usage_bytes{container=~"myapp"}[5m]) > 5e6,比静态阈值靠谱得多 - 报警时附带
curl http://localhost:6060/debug/pprof/heap?debug=1的原始输出快照(base64 编码后塞进 alert annotation),避免事后查不到现场 - 别忘了加
container_memory_working_set_bytes对比——如果 working_set 远小于 usage,说明有大量 page cache 或 inactive file mapping,未必是泄漏
工具链里最易被跳过的一步:验证 GC 是否真被触发
有些容器里 GC 频率极低(比如 GOGC=1000),导致对象堆积在堆上不回收,看起来像泄漏。但 runtime.ReadMemStats 的 NextGC 和 HeapAlloc 差值很大时,GC 其实根本没跑。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 启动参数加
-gcflags="-m -m"看编译期逃逸分析,确认关键结构体没意外逃逸到堆上 - 运行中 curl
http://localhost:6060/debug/pprof/goroutine?debug=2,搜runtime.gcBgMarkWorker,如果数量为 0 或长期 - 临时手动触发 GC:
curl -X POST http://localhost:6060/debug/pprof/heap?debug=1不起作用,得用curl -X POST http://localhost:6060/debug/pprof/heap?debug=1 -d "force=1"(注意 force 参数) - 容器内存 limit 设得太小(比如 512MB)会导致 Go runtime 主动降低 GC 频率保命,此时看
GODEBUG=gctrace=1日志里会有scvg相关提示
真正难的不是抓到泄漏点,而是区分“增长合理但慢”和“增长失控但隐蔽”。比如 mmap 分配的匿名页,在容器里不会被 cgroup memory controller 立即计入 working_set,可能延迟 2~3 分钟才体现——这个时间差,足够掩盖前两次报警。









