db.Stats()是第一排查入口,因它暴露WaitCount、WaitDuration和MaxOpenConnections三指标,可早于日志发现连接排队或泄漏;WaitCount>0且WaitDuration持续增长表明连接被长期占用未归还。

为什么 db.Stats() 是第一排查入口
连接池本身不记录 SQL,但会暴露“谁在等连接”——慢 SQL 占着连接不释放,就会让后续请求卡在 WaitCount 上。直接看 db.DB().Stats() 返回的结构体,重点关注三个字段:WaitCount(排队次数)、WaitDuration(总排队耗时)、MaxOpenConnections(是否已打满)。如果 WaitCount > 0 且 WaitDuration 持续增长,说明有连接被长期占用,不是数据库慢,而是某条 SQL 执行完没归还连接。
- 别只查 MySQL 的
SHOW PROCESSLIST:它只显示“当前正在跑的”,而 GORM 连接可能已执行完 SQL、正卡在 Go 层反序列化或业务逻辑里 - 每 5 秒调一次
db.DB().Stats()并打点上报,比日志更早发现连接堆积趋势 -
OpenConnections长期等于MaxOpenConnections,但IdleConnections为 0,基本可断定有连接泄漏或慢查询未结束
SlowThreshold + Trace 日志必须配对启用
仅设 LogMode(logger.Warn) 不会触发慢查询记录,GORM 默认的 SlowThreshold 是 200ms,但该阈值不生效除非你显式设置。必须同时配置:
-
logger.Default.LogMode(logger.Warn)或自定义 logger 的LogLevel≤ Warn -
SlowThreshold: 100 * time.Millisecond(OLTP 场景建议 100ms 起) - 确保
Trace方法中调用fc()获取真实 SQL 和rowsAffected,否则日志里只有占位符
这样打出的日志形如:[WARN] [247ms] SELECT * FROM users WHERE id = ?,能直接定位到哪条语句拖住了连接。
Preload 和事务内子查询最容易藏匿慢连接
Preload 的子查询默认不继承主查询的 Debug() 或 SlowThreshold,也不走同一 logger 实例,容易漏监控。事务里嵌套的 db.Create() 或 db.Model().Updates() 同理。
- 全局 logger 是最省事的方案:
logger.Default.LogMode(logger.Warn)之后所有路径(包括 Preload、Joins、事务内子查询)都会被覆盖 - 若用自定义 logger,务必重写
Trace方法,并确认ctx是从外层传入的,而不是新建 context.Background() - 检查
Preload("Orders").Find(&users)是否真的发了子查询:没看到SELECT * FROM orders WHERE user_id IN (?)日志,说明 Preload 被跳过,连接根本没被它占用——别冤枉它
连接没释放?先盯住 defer 和 panic 恢复路径
GORM 连接归还是靠 defer 和函数返回,一旦中间有 panic 且没被 recover,或者用了 db.Session(&gorm.Session{...}) 但忘了手动 Close,连接就永远卡住。
- 所有带
db.Transaction()的代码块,必须确保defer tx.Rollback()在tx.Commit()后不会被执行,否则 rollback 会把连接锁死 - 避免在事务内调用外部 HTTP 或阻塞 IO:它们不释放连接,但会让连接池等待超时
- 上线前用
pprof抓 goroutine 堆栈,搜索database/sql相关调用,看是否有大量conn.exec卡在read或write
连接池问题从来不是孤立的,它暴露的是 SQL 执行链路中某个环节的阻塞——要么是数据库真慢,要么是 Go 层没放手。日志和指标要一起看,单看一个很容易误判。











