Go 后端内存排查:pprof 定位泄漏与 OOM 兜底
Go 后端内存排查:pprof 定位泄漏与 OOM 兜底
点餐小程序的 Go 后端跑在 2 核 2G 的 Flexus L 上,某天晚上突然重启,查日志只看到一行 “out of memory: Killed process”。内存从几百兆一路涨到 2G,没人发现,直到 OOM Killer 动手。事后我用 pprof 把泄漏找了出来,顺手给服务加了两道兜底。
第一步是复现与定位。Go 自带 pprof,在 main 里 import _ “net/http/pprof” 并起一个调试端口,就能在 /debug/pprof/heap 看到实时堆内存。更直接的办法是抓两次 heap profile 对比:curl -o heap1.out http://localhost:6060/debug/pprof/heap,隔十分钟再抓 heap2,然后 go tool pprof -base heap1.out heap2.out,用 top 命令看增长最多的调用栈。我那次抓出来,增长最快的是一个被遗忘的定时器 goroutine——每分钟启动一个 ticker,却没有在请求结束时 Stop,导致 goroutine 和它持有的连接对象越积越多。
第二步是修泄漏的常见模式。Go 内存泄漏大多不是真的"丢了指针",而是对象还被某个地方引用着,GC 回收不掉。最常见的三类:goroutine 泄漏(channel 没人接收、select 永远等不到、ticker 没 Stop)、缓存无上限(map 只进不出,必须加容量上限和过期淘汰)、HTTP 响应体没 Close(resp.Body 不关,连接和内存都回不去池里)。修的时候按"谁创建谁释放"的原则,defer 能解决的就地解决;缓存则用带 TTL 的结构,超过阈值直接淘汰。
第三步是给小内存服务器加兜底。定位工具再好,也防不住下一次意外增长。我的做法是三层:第一层是 Go 运行时的 GOMEMLIMIT,设成总内存的七成左右,GC 会提前发力回收;第二层是 systemd 的 MemoryMax,写进 service 文件,超了就重启服务而不是让 OOM Killer 随机杀进程;第三层是监控告警,写个每分钟跑的脚本看 RSS,超过阈值就发消息提醒,宁可早知道不要半夜被叫醒。
第四步是日常巡检的习惯。2G 内存的服务器经不起"上线就忘",我每周看一次三组数:go_memstats_alloc_bytes 看 Go 堆,RSS 看进程真实占用,系统剩余内存看整机余量。配合 pprof 的 goroutine 数(goroutine 数量异常上涨往往是泄漏的第一信号),大部分问题在变大之前就能发现。
内存泄漏不可怕,可怕的是没有观测手段。pprof 定位、修三类常见泄漏、再加 GOMEMLIMIT、MemoryMax 和告警三道兜底,2G 小服务器也能跑得稳。排查工具是手段,让问题在造成影响之前被发现,才是目的。
- 点赞
- 收藏
- 关注作者
评论(0)