连接泄漏表现为InUse持续接近MaxOpenConnections、Idle≤2、WaitCount累加,需通过db.Stats()实时监控;Rows()必须显式检查并关闭,gorm.Open()仅限启动时调用一次,Transaction()须配对Commit/Rollback。

连接泄漏不是“查不到错误就没事”,而是连接数缓慢爬升、WaitCount持续增长、最终 Too many connections 爆发——必须从 db.Stats() 的实时指标切入,而非等崩溃才动手。
怎么看连接池是不是在悄悄泄漏
别只盯着日志有没有报错。连接泄漏的典型体征是“安静地变慢”:接口 P99 延迟缓慢上升、内存不涨但 db.Stats().InUse 持续贴近 MaxOpenConnections、WaitCount 不断累加,且 WaitDuration 越来越长。
-
stats := db.Stats()必须在关键路径(如 HTTP middleware 或定时任务)里定期打点,至少每 10 秒采集一次,写入 Prometheus 或本地日志 - 重点关注三个值:
stats.InUse(当前占用)、stats.Idle(空闲)、stats.WaitCount(累计等待次数)——如果InUse长期 > 90%MaxOpenConnections且Idle始终 ≤ 2,基本可判定连接没被归还 - 用
curl -s http://localhost:6060/debug/pprof/goroutine?debug=1 | grep -c "database/sql"快速确认是否有大量 goroutine 卡在database/sql.(*DB).conn上
为什么 rows.Close() 漏了就会泄漏
GORM 查询返回的 *gorm.DB 实例本身不持有连接,但 Rows(比如 db.Raw(...).Rows() 或 db.Model(...).Select(...).Rows())底层会从连接池借出一个 *sql.Conn,这个连接只有显式调用 rows.Close() 才会归还。GORM 的链式调用(如 First()、Find())内部已自动处理,但裸 Rows() 不会。
- 错误写法:
rows, _ := db.Table("users").Select("id,name").Rows(); defer rows.Close()—— 如果rows是nil(比如 SQL 报错),defer rows.Close()会 panic;如果函数提前 return,defer可能没执行到 - 正确写法:必须用
if rows != nil { defer rows.Close() }包裹,或更稳妥地写成if rows != nil { _ = rows.Close() }放在逻辑末尾 - 注意:GORM v2 的
Scan()方法不返回Rows,所以不涉及此问题;但凡看到.Rows(),就要条件检查 + 显式关闭
为什么反复 gorm.Open() 比漏关 Rows 更危险
这不是“多开几个连接”的问题,而是每次 gorm.Open() 都新建一个 *sql.DB 实例,每个实例自带独立连接池。旧池子不会自动销毁,db.Close() 又常被忽略,导致文件描述符和 MySQL 连接数双爆表。
- 典型误用场景:HTTP handler 里写
db, _ := gorm.Open(...),或者在 goroutine 内部初始化 DB 实例 -
gorm.Open()必须且只能在应用启动阶段调用一次,用包级变量或依赖注入容器管理生命周期 - 验证是否违规:全局搜索项目代码里的
gorm.Open,出现超过一次就是高危;再搜db.Close(),如果它出现在非main()或非服务 shutdown 流程里,说明资源释放逻辑混乱
修复后怎么确认泄漏真止住了
重启服务只是清零计数器,不是解决问题。真正有效的验证,是在压测中观察连接池指标是否回归稳态。
- 用
ab -n 1000 -c 50 http://localhost:8080/api/users模拟并发请求,同时持续轮询db.Stats() - 健康表现应为:
InUse在 10–30 之间波动(取决于SetMaxOpenConns设置),Idle维持在 5–10,WaitCount增长趋近于 0,WaitDuration基本为 0 - 特别注意:如果压测后
InUse没回落,或Idle长期为 0,说明仍有连接未归还,大概率是某处Rows或事务(db.Transaction())没正确结束
最易被忽略的一点:GORM 的 Transaction() 一旦开始,必须显式 Commit() 或 Rollback(),否则该事务持有的连接永远不会释放——哪怕函数 return 了,连接也卡在 InUse 状态,且不会出现在任何错误日志里。











