是,goroutine泄漏确实会导致RSS持续上涨;主因并非单个goroutine的2KB栈内存,而是其长期持住channel、timer、HTTP连接、闭包大对象等资源,这些才是RSS飙升的主力,heap profile稳定而RSS上升即为典型特征。
goroutine 泄漏确实会导致 rss 持续上涨,但不是因为每个 goroutine 本身占多少内存(默认栈 2kb),而是它钉住了背后的 channel、timer、http connection、闭包捕获的大对象等资源——这些才是真正吃 rss 的主力。单看 heap profile 内存稳定,rss 却一路走高,八成是这个原因。
runtime.NumGoroutine() 涨了,但不确定是不是真泄漏
数字变大不等于泄漏,得看趋势和上下文:
- 刚启动 HTTP 服务时
runtime.NumGoroutine()跳到 100+ 是正常的,pprof、healthz、日志 flusher 都在初始化 - 真正要盯的是三种情形:单次请求返回后数字没回落;压测结束 30 秒后仍比空闲态高一大截;稳定运行几小时呈单调上升(如 60 → 240 → 960)
- 建议在关键路径打三处日志:
log.Printf("goroutines@idle: %d", runtime.NumGoroutine())、@start、@done,别只采一次样
/debug/pprof/goroutine?debug=2 显示大量 chan receive/select
这是泄漏最典型的 pprof 表现,但光看到“卡住”还不够,得确认是不是新长出来的:
- 先抓 baseline 快照(空闲态),压测或跑一段时间后再抓一次,用
diff工具比对新增 goroutine 的堆栈 - 重点关注阻塞时长字段:出现
432000s(5 天)、1209600s(14 天)这种量级,基本可断定是泄漏,不是慢操作 - 常见高危模式:
nil chan上收发、无缓冲chan单向操作、有缓冲chan写满未消费、select所有case不可达且无default - 如果堆栈集中在
net/http.readLoop或golang.org/x/crypto/ssh,优先检查服务端是否设置了ReadTimeout/IdleTimeout
goleak.VerifyNone 在测试里没报,但线上还是涨
这不是 goleak 失效,而是它检测逻辑和线上场景不匹配:
-
goleak.VerifyNone(t)只捕获测试函数退出时“还活着”的 goroutine;如果泄漏 goroutine 在t结束前就因超时/关闭 channel 退出了,它就看不见 - 正确做法是确保待测逻辑完整执行完再验证,比如加
time.Sleep(10ms)或等done chan关闭;更稳的是用goleak.VerifyTestMain(m)包裹整个TestMain - 注意误报:第三方库(如
http.Server、time.AfterFunc)启动的 goroutine 需显式用goleak.IgnoreTopFunction()过滤
Go 1.24+ 的 /debug/pprof/goroutineleak 端点怎么用
这是目前最接近“自动识别泄漏”的手段,但需要配环境、等 GC:
- 启动时加环境变量:
GODEBUG=goleak=1(Go 1.24+)或GOROUTINELEAK=1(gotip 实验版) - 首次访问
/debug/pprof/goroutineleak?debug=1前,手动触发一次runtime.GC(),否则可能为空 - 返回堆栈中带
_Gleaked状态标记的 goroutine,就是被 GC 层确认“不可达且阻塞”的泄漏项 - 它不抓死循环或无限重试类泄漏,只抓卡在
chan、select、semacquire等同步原语上、且无退出路径的 goroutine
最常被忽略的一点:泄漏 goroutine 往往不直接出现在业务 handler 里,而藏在下游 client、ticker、ssh.Dial、甚至 http.Response.Body 未关闭导致的 persistConn 持有中——查的时候别只盯着自己写的 go func()。











