最典型的“伪高并发”是中间件中数据库连接未释放导致连接池耗尽:需检查所有 db.Query/db.BeginTx 是否配对 defer rows.Close()/tx.Rollback(),禁用可疑中间件验证,并用 pprof 定位卡住的 goroutine。

中间件里调了数据库但没关连接,池子就空了
这是最典型的“伪高并发”现场:Gin 每个请求确实跑在独立 goroutine 里,但某个中间件(比如鉴权、日志、审计)里直接用了 db.Query 却忘了 rows.Close(),或用了 tx.Commit() 但没 defer tx.Rollback()。连接一借不还,几十个并发就能把池子占满。
常见表现是日志里反复出现 Connection is not available, request timed out after 30000ms,而 redis-cli INFO clients 或 show processlist 显示大量 idle 连接挂着不动。
- 检查所有中间件代码,确认每处
db.Query/db.Exec/db.BeginTx都配对了defer rows.Close()或defer tx.Rollback() - 禁用可疑中间件做对比测试:注释掉后压测,若连接数不再增长,基本锁定问题点
- 用
pprof抓 goroutine 堆栈:curl "http://localhost:6060/debug/pprof/goroutine?debug=2",搜索Query或Begin关键字,看哪些 goroutine 卡在数据库调用上没退出
ParseForm 或 ShouldBindJSON 提前读了 Body,导致超时/限流失效
很多中间件为了取参数(如签名验证),会提前调 c.Request.ParseForm() 或 c.ShouldBindJSON(&v)。这会导致 c.Request.Body 被消耗一次——后续再想用 http.MaxBytesReader 包装就完全无效,大请求体照单全收,内存暴涨 + goroutine 长时间阻塞在 I/O 上,间接拖垮连接池。
典型症状是:压测时 CPU 不高,但 top 显示大量 goroutine 处于 IO wait 状态,netstat -an | grep :8080 | wc -l 连接数持续攀升不释放。
- 所有需要读 body 的中间件,必须放在
http.MaxBytesReader包裹之后;推荐统一用http.MaxBytesHandler包裹整个gin.Engine - 若必须接口级差异化限制(如
/upload允 100MB,/login仅 2MB),则在对应 handler 开头第一行手动重赋值:c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 2<em>1024</em>1024) - 避免在中间件中调
c.ShouldBindJSON();改用json.NewDecoder(c.Request.Body).Decode()并确保已提前包装MaxBytesReader
context.WithValue 存了大对象,GC 不回收,goroutine 堆积
context.WithValue 看似安全,但一旦往 *gin.Context 里塞了大 struct、未关闭的文件句柄、或整个 *sql.DB 实例,就会让该 context 无法被 GC 回收。如果这个 context 又被传给了异步 goroutine(比如发消息、写日志),那它就会长期存活,连带占用的数据库连接、HTTP 连接、内存全部卡死。
现象是:服务运行几小时后连接池缓慢耗尽,pprof 显示大量 goroutine 堵在 runtime.gopark,且 heap profile 里有异常大的对象残留。
- 只用
context.WithValue存轻量元数据(如userID、traceID),禁止存结构体指针、资源句柄、连接对象 - 所有异步 goroutine 必须显式接收
context.Context并监听Done(),执行完立即 return,避免闭包隐式持有 context - 用
go tool pprof -http=:8081 binary heap.pprof查看 top allocs,重点关注非标准类型(如自定义 struct)的 size 和 count
WebSocket 升级后没 close,goroutine 和连接双泄漏
Gin 不支持 WebSocket 原生路由,必须用 gorilla/websocket 手动升级。但很多人在 upgrader.Upgrade() 后直接写 conn.ReadMessage(),却忘了加 defer conn.Close(),也没处理连接异常中断。结果每个 WebSocket 连接都卡住一个 goroutine + 一个 TCP 连接 + 一个数据库连接(如果中间件里开了事务)。
现象是:netstat -an | grep :8080 | grep ESTABLISHED | wc -l 持续上涨,redis-cli CLIENT LIST 里看到大量 idle=3600 的 client,但业务实际没人在线。
- Upgrade 成功后,立刻用
defer conn.Close(),并在for循环里用if err != nil { break }退出,防止 panic 后 defer 不执行 - 不要在 WebSocket handler 里调任何 Gin 中间件逻辑(比如 auth 中间件),它们依赖 HTTP 生命周期,此时
c已失效 - 用
conn.SetReadDeadline()和conn.SetWriteDeadline()主动控制连接生命周期,避免长连接无限挂起
defer rows.Close() 缺失、一次 ParseForm() 误用、或一个 context.WithValue 存错对象,三者叠加后在高并发下集中爆发的结果。查的时候别只盯着池子大小调参,得顺着 goroutine 栈、连接 idle 时间、heap 分配痕迹一层层往下凿。


















