goroutine泄漏比CPU占用更危险,因高负载下OOM或响应变慢常源于goroutine持续增长未回收,常见于未关闭的HTTP连接、未close的channel或未取消的time.AfterFunc定时任务。
高负载下服务突然 OOM 或响应变慢,大概率不是 CPU 扛不住,而是 goroutine 持续增长没回收。常见于未关闭的 http.Client 连接、忘记 close() 的 channel、或用 time.AfterFunc 启动但没取消的定时任务。
实操建议:
pprof:在启动时注册 net/http/pprof,用 curl http://localhost:6060/debug/pprof/goroutine?debug=2 查看全量 goroutine 堆栈
go fn():所有异步逻辑必须带 context 控制生命周期,例如 go func(ctx context.Context) { ... }(req.Context())
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
}sync.Pool 适合复用短期、结构固定、创建开销大的对象(如 JSON 解析器、buffer),但不适合长期持有或含指针的复杂结构。Go 1.22+ 中,如果 Pool.Get() 返回 nil 频繁,说明对象复用率低,此时分配新对象比查 pool 更快。
实操建议:
*bytes.Buffer、*json.Decoder
Init 函数里不要做 I/O 或锁操作GODEBUG=gctrace=1 下的 GC pause 时间,确认 pool 确实降低了对象分配压力
HTTP handler 里直接 panic 会触发 http.Server 的默认 recover,但无法记录堆栈、丢失请求上下文,且高并发 panic 可能压垮日志系统。更糟的是,recover 后若没重置 response writer,可能写出重复 header 导致连接异常关闭。
实操建议:
debug.Stack() 和 request ID,并返回 500recover() 后继续业务逻辑——recover 只是止血,不是容错json.Unmarshal)前先 validate 输入长度和格式,避免 panic 入口过深http.StripPrefix + http.FileServer 时,务必 wrap handler,否则文件路径遍历 panic 会暴露服务细节单靠 top 看 CPU 或内存占用,根本定位不到 Go 服务的真实瓶颈。比如 runtime.mallocgc 占高,可能是频繁小对象分配;selectgo 占高,说明 channel 等待严重;而 MemStats.Alloc 持续上涨但 HeapInuse 稳定,说明对象没逃逸但被长期引用。
实操建议:
go tool trace 分析调度延迟和 GC STW,重点关注 “Goroutine analysis” 和 “Network blocking profile”runtime.ReadMemStats 上报关键指标到监控系统,尤其关注 NumGC 和 PauseTotalNs
真正卡住高负载 Go 服务的,往往不是某行代码慢,而是多个小决策叠加后的资源滞留:一个没 cancel 的 context、一次没 close 的 response body、一个没 reset 的 buffer —— 它们各自看起来无害,合起来就是雪崩前夜。