立即执行三类OS层熔断:1.用docker update锁死内存上限;2.在网关限流阻断goroutine新建洪流;3.对失控实例执行kill -STOP冻结调度,再通过pprof定位[chan receive]等阻塞点。
线上服务突然出现 goroutine 数从几百飙到 10 万+,内存 rss 在几分钟内冲破容器 limit 被 oom killer 杀掉——这不是要你立刻翻源码,而是先让服务活下来。止血必须快于诊断,且手段要能绕过代码逻辑直接生效。
立刻执行的三类 OS 层熔断操作
别碰应用代码,先从系统和基础设施层压制增长源头:
- 用
docker update --memory-swap=2g --memory=2g <container_id></container_id>锁死内存上限(哪怕临时降为 1.5G),触发内核级 cgroup 硬限,比任何 Go 参数都可靠 - 在入口网关(Nginx / Envoy)对目标服务路径加
limit_req zone=burst burst=10 nodelay,把突增流量压成匀速队列,阻断 goroutine 新建洪流 - 对已失控实例执行
kill -STOP <pid></pid>(不是kill -9),暂停所有 M 线程调度,冻结当前 goroutine 状态,避免继续分配栈内存或持有堆对象
用 debug/pprof 快速定位阻塞点(不依赖重启)
只要进程还在运行(哪怕已 kill -STOP),就能抓到真实现场。重点不是看“有多少”,而是看“卡在哪”:
- 执行
curl 'http://localhost:6060/debug/pprof/goroutine?debug=2',搜索关键词[chan receive]、[select]、[IO wait]—— 出现频次最高的那一行调用栈,就是泄漏根因所在函数 - 若大量 goroutine 停在
net/http.(*persistConn).readLoop或database/sql.(*DB).conn,说明下游超时未设,正在无限等待网络/数据库响应 - 对比正常时段的
goroutine快照:如果新增 goroutine 全部集中在某个 handler 函数里,且该函数内有go process(...)无限制启动,基本可锁定
紧急绕过代码的 goroutine 限流补丁
来不及发版?在进程存活时热插一个轻量级控制层,无需改业务逻辑:
- 在 HTTP handler 入口加全局信号量:
var sem = make(chan struct{}, 100),每个请求进来先sem ,处理完再 <code>,硬控并发数 - 对已存在但未加 context 的 I/O 调用,用
select包一层超时:select { case result := - 禁用所有非必要异步:临时注释掉所有
go func() { ... }(),改用同步调用;对必须异步的,统一走一个固定 size 的 worker pool:workerPool := make(chan Msg, 1000)
为什么 GOMEMLIMIT 和 GOGC 在 OOM 时几乎无效
这两个环境变量只影响 Go runtime 的 GC 行为,对 OOM Killer 完全无感:
-
GOMEMLIMIT控制的是 GC 触发阈值,但若 goroutine 持有大量大对象(如未释放的[]byte、未 close 的http.Response.Body),堆外内存(runtime.MemStats.Sys - runtime.MemStats.HeapSys)仍会持续上涨 -
GOGC=10只会让 GC 更频繁,但若对象存活率高(比如缓存未淘汰、channel 未关闭),反而增加 stop-the-world 时间和元数据开销 - 真正被 OOM Killer 杀的,90% 是因为
ps -o rss,pid,comm -p <pid></pid>显示的 RSS 超限,而 RSS 包含 mmap 区域、cgo 分配、未归还的页等,GC 根本管不到
最易被忽略的一点:goroutine 泄漏往往不是“没写 cancel”,而是“写了 cancel 却没监听”。只要有一个 goroutine 卡在无超时的 http.Get 或阻塞 channel 读,它就会一直活着,拖住整个引用链上的内存。防御的关键不在事后回收,而在新建时就设闸。

















