连接池爆满的典型表现是服务日志反复出现“sql: connection refused”或“context deadline exceeded”,Gin中间件统计大量请求卡在数据库层,DB.Stat().OpenConnections持续等于MaxOpenConns且不回落。

连接池爆满的典型表现是什么
服务日志里反复出现 sql: connection refused 或 context deadline exceeded,Gin 中间件统计到大量请求卡在数据库层,DB.Stat().OpenConnections 持续等于 MaxOpenConns 且长时间不回落。这不是偶发超时,而是连接被长期占住、无法释放——说明连接没归还,或者归还太慢。
为什么 GORM 的连接会“不归还”
GORM 自身不会主动泄漏连接,但以下几种写法会让连接卡在未关闭状态:
- 用
db.Raw()执行查询后,忘记调用.Rows()并显式rows.Close();Raw返回的是*sql.Rows,不是自动管理的结构体切片 - 在事务中使用
tx.Find(&v)后,没调用tx.Commit()或tx.Rollback(),导致事务连接一直挂起 - 把
*gorm.DB实例(尤其是带Session或WithContext的)误当成单例全局复用,而它内部持有了可能未释放的连接上下文 - 在 goroutine 里直接用
db.WithContext(ctx)但 ctx 已 cancel,GORM 不会主动中断并释放底层连接,连接可能滞留在池中等待超时
怎么配才不算“瞎设”连接池参数
MaxOpenConns 不是越大越好,它受 MySQL 侧 max_connections 和应用并发模型共同约束。一个常见错误是把 MaxOpenConns 设成 1000,但实际业务峰值并发只有 200,结果大量空闲连接占着资源,反而挤压其他服务或触发 MySQL 拒绝新连接。
推荐做法是按压测结果反推:
- 先设保守值:
MaxOpenConns = 50,MaxIdleConns = 20,ConnMaxLifetime = 1h - 用
ab或hey压测真实接口,观察DB.Stat()中InUse和Idle的波动比例 - 若
InUse长期接近MaxOpenConns,且WaitCount持续增长,说明真不够用;若Idle占比长期 >80%,说明设高了 - 注意
ConnMaxIdleTime必须小于 MySQL 的wait_timeout(默认 8h),否则空闲连接会被服务端主动断开,GORM 归还时会报错
SQL 日志里看不到慢查询,但连接就是不释放
这种情况大概率是连接被阻塞在「非 SQL 执行阶段」:比如事务没提交、Rows.Scan 没遍历完、或用了 db.First().Error 但忽略了 rows.Close()。GORM 的 Logger 默认只记录 SQL 执行完成事件,不记录连接获取/归还时机。
最有效的排查方式是加一行诊断代码:
go
db.Callback().Create().After("gorm:create").Register("log_conn_acquire", func(db *gorm.DB) {
fmt.Printf("[CONN] acquired, in use: %d, idle: %d\n",
db.ConnPool.(*sql.DB).Stats().InUse,
db.ConnPool.(*sql.DB).Stats().Idle)
})
再配合定期打印 db.Stat(),就能定位哪类操作后连接数突增不降。别依赖日志猜,连接生命周期必须可观测。
真正棘手的从来不是参数调多大,而是连接从哪来、在哪卡、为何不走——只要能抓到一次未归还的完整调用栈,问题就解决了一半。











