一眼看出是连接池耗尽而非SQL慢:InUse接近MaxOpenConnections、Idle==0且不回落、WaitCount>0并持续增长,三者同时出现说明请求卡在“等连接”阶段,SQL尚未执行。

怎么一眼看出是连接池耗尽而不是SQL慢
执行 db.Stats() 查看连接池实时状态,重点关注三个字段:InUse、Idle、WaitCount。当 InUse 接近或等于 MaxOpenConns,Idle 持续为 0,且 WaitCount 不断上涨、WaitDuration 持续增长,就说明请求卡在“等连接”阶段——SQL 根本还没执行。
常见误判现象包括:
-
context deadline exceeded频繁报出,但 MySQL 的SHOW PROCESSLIST里看不到长事务 - 接口偶发超时,重启服务立刻恢复,几小时后复现
- Prometheus 中
sql_db_wait_count_total持续爬升,而sql_db_open_connections卡死在上限值
为什么每次请求都 new gorm.DB 会直接压垮数据库
每次调用 gorm.Open() 都会新建一个独立的 *gorm.DB 实例,背后绑定一个全新的 *sql.DB 连接池。多个池之间无法共享连接,也无法复用空闲连接。
后果很直接:
- MySQL 服务端连接数暴涨,很快触发
Too many connections错误(尤其高并发下) - 每个池默认
MaxOpenConns = 0(即不限制),实际可能打开数百上千个连接 - 全局钩子(如
BeforeCreate)需重复注册,逻辑错乱;日志、PrepareStmt 等配置也被覆盖
正确做法是全局单例初始化:用 sync.Once 保证只调用一次 gorm.Open(),后续所有地方复用同一个 *gorm.DB 实例。
连接池参数不设等于没设
GORM 底层复用 database/sql,但它的连接池参数全靠手动设置,GORM 自身不提供默认合理值。不设就等于放任自流。
必须在 gorm.Open() 后立刻配置底层 *sql.DB:
-
sqlDB.SetMaxOpenConns(20):硬性限制最大并发连接数,避免打爆 DB -
sqlDB.SetMaxIdleConns(10):控制空闲连接上限,防止资源闲置 -
sqlDB.SetConnMaxLifetime(60 * time.Minute):强制连接定期重建,避免被中间件或防火墙静默断连 -
sqlDB.SetConnMaxIdleTime(30 * time.Second):及时回收长期空闲连接,减少无效维持
注意:SetMaxOpenConns 设太小会导致排队等待;设太大又可能让 DB 吃不消。建议从 10–30 起步,按压测结果微调。
事务没提交/回滚是最隐蔽的连接杀手
事务开启后,连接被绑定在 *sql.Tx 上,直到显式调用 Commit() 或 Rollback() 才归还给连接池。漏掉任何一个,连接就永远卡住。
典型踩坑点:
-
if err != nil { return }直接返回,没写tx.Rollback() -
defer tx.Commit()被 panic 绕过,实际没执行 - 事务内调用外部 HTTP 服务,超时后没兜底
tx.Rollback()
这类问题不会立刻暴露,而是随着并发上升缓慢堆积 —— InUse 慢慢涨到顶,Idle 归零,WaitCount 开始跳动。排查时重点扫所有 Begin() 调用点,确保每条路径都有明确的 Commit 或 Rollback。


















