我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录Go 服务容器内存持续上涨但 heap profile 是平的3 类非堆内存一、先说清 heap profile 看不见什么二、第 1 类goroutine 栈内存三、第 2 类mmap / cgo 直接分配四、第 3 类runtime 保留但未归还给 OS五、把三类收成一张排查表六、总结Go 服务容器内存持续上涨但 heap profile 是平的3 类非堆内存上周告警某 Go 服务常驻内存 RSS 从 200MB 一路涨到 1.2GB眼看要撞容器 limit。我第一反应是经典的 Go 内存泄漏熟练地拉起 pprofcurlhttp://localhost:6060/debug/pprof/heapheap.prof go tool pprof-topheap.prof结果inuse_space只有 150MB而且从头到尾不涨。这就很反直觉了RSS 涨了 1GB堆上却只有 150MB。说明漏的不是堆内存。这篇把这类heap profile 看不见的内存拆成 3 类每类给识别方法和一个可复现实验。一、先说清 heap profile 看不见什么runtime.MemStats和 pprof heap 只统计Go 堆由分配器管理的对象。而进程的 RSSResident Set Size真实占用的物理内存包含的东西更多一句话heap profile 是Go 堆的快照不是进程内存的账单。RSS 涨而 heap 平漏点必然在上面三块非堆区域里。二、第 1 类goroutine 栈内存每个 goroutine 有独立的栈初始 2KB按需增长到 1GB 上限。goroutine 泄漏时堆可能不涨栈不算堆但 RSS 一路涨。识别方法# 看 goroutine 数量对比业务量是否匹配curlhttp://localhost:6060/debug/pprof/goroutine?debug2|head# 或用 pprof 看栈内存总量go tool pprof-sample_indexinuse_space http://localhost:6060/debug/pprof/heap# 专门看栈runtime.memstats 的 StackInusecurl-shttp://localhost:6060/debug/pprof/heap|go tool pprof-top/dev/stdin2/dev/null|grep-istack更直接的判据是runtime.MemStats.StackInuse。如果它持续上涨、goroutine 数也在涨基本实锤。可复现实验起 10 万个阻塞在 channel 上的 goroutine观察 RSS。packagemainimport(fmtruntimetime)funcmain(){block:make(chanstruct{})fori:0;i100000;i{gofunc(){-block}()// 永久阻塞栈被保留}time.Sleep(time.Second)varm runtime.MemStats runtime.ReadMemStats(m)fmt.Printf(goroutines%d StackInuse%.1f MB HeapAlloc%.1f MB\n,runtime.NumGoroutine(),float64(m.StackInuse)/1024/1024,float64(m.HeapAlloc)/1024/1024)// 典型输出goroutines100001 StackInuse~204 MB HeapAlloc~0.2 MB}现象很明确10 万 goroutine 吃掉了约 200MB 栈内存而 HeapAlloc 几乎没动。这就是 heap profile 平、RSS 涨的经典形态。三、第 2 类mmap / cgo 直接分配走 cgo 调用比如 SQLite、某些图像库、OpenSSL 封装时C 侧用malloc分配的内存完全不经过 Go 分配器pprof 半个字节都看不到。识别方法看/proc/pid/smaps里匿名映射的Rss。PID$(pgrep-fmyservice)# 汇总匿名内存Anonymous 段—— cgo/mmap 分配都在这awk/^Anonymous:/ {sum$2} END {print Anonymous total:, sum/1024, MB}/proc/$PID/smaps# 对比 Go 自己报告的堆curl-slocalhost:6060/debug/pprof/heap|go tool pprof-top/dev/stdin2/dev/null|head-5如果Anonymous总量远大于 heap 报告的 inuse且你的程序用了 cgo嫌疑就锁定这块。注意GODEBUGmadvdontneed1只影响 Go 是否把空闲页还给 OSGo 1.12~1.15 的行为差异对 cgo 的 malloc 无效。cgo 的内存要么靠 C 侧自己 free要么靠定期重启兜底。四、第 3 类runtime 保留但未归还给 OSGo 的分配器从 OS 申请内存Sys用完不一定马上还给 OS——它留着做下次分配的缓存。所以会出现HeapIdle空闲但保留很大HeapReleased很小RSS 高居不下varm runtime.MemStats runtime.ReadMemStats(m)fmt.Printf(HeapInuse%.0fMB HeapIdle%.0fMB HeapReleased%.0fMB HeapSys%.0fMB\n,mb(m.HeapInuse),mb(m.HeapIdle),mb(m.HeapReleased),mb(m.HeapSys))判据如果 RSS ≈ HeapSys而 HeapInuse 很小、HeapIdle 很大 → 是分配器保留未归还不是泄漏。修法有两个方向# ① 让 runtime 更积极地归还Go 1.16 默认已开启旧版本可显式打开GODEBUGmadvdontneed1./myservice# ② 调 GC 触发时机让空闲堆早点被回收GOGC50./myservice# 更频繁 GCGOMEMLIMIT400MiB ./myservice# 软内存上限逼近时强制回收用GODEBUGmadvdontneed1做对照实验同一份负载跑两次一次带这个变量、一次不带对比 RSS 曲线。如果开了之后 RSS 明显回落就证明是保留未归还而不是真泄漏。五、把三类收成一张排查表内存类别pprof heap 可见关键指标判据修法goroutine 栈✅ stackNumGoroutine/StackInusegoroutine 数与业务量不匹配且持续涨修泄漏的 goroutinemmap / cgo❌/proc/pid/smapsAnonymousAnonymous ≫ heap inuse 且有 cgoC 侧 free 或定期重启runtime 保留❌HeapIdle/HeapReleasedRSS≈HeapSys 且 Inuse 小GODEBUG / GOMEMLIMIT结论必须分三档写这也是我这次复盘的教训已证实RSS 涨而 heap inuse 平 —— 三类非堆内存中必有一类已排除Go 堆泄漏pprof 反复采样 inuse 不涨排除待确认具体是哪一类 —— 要结合smapsNumGoroutineHeapIdle三个指标交叉定位。六、总结heap profile ≠ 进程内存账单RSS 涨而 heap 平问题一定在非堆区域三类非堆内存goroutine 栈看 NumGoroutine/StackInuse、cgo/mmap看 smaps 的 Anonymous、runtime 保留看 HeapIdle/HeapReleased10 万阻塞 goroutine 能吃掉约 200MB 栈内存而 HeapAlloc 几乎不动 —— 这是最常见的误判现场别一看到 RSS 涨就上 pprof 找堆先把三类非堆内存排一遍。留个问题给你你们服务的 RSS 涨最后查出来是 heap 占的还是 goroutine 栈占的有没有遇到过 cgo 泄漏这种pprof 里根本不存在的情况
阅读完成 · 觉得有帮助?