← 中文资源

简体中文版本

Go — database/sql 连接池因未关闭 Rows 耗尽

诊断 Go 的 sql: connection pool exhausted,找到缺 defer rows.Close 的循环,并用确定性资源处理恢复连接池。

阅读英文原文

症状

  错误来自连接池而非数据库本身:sql: connection pool exhausted。两路遥测把它坐实:应用侧 db.Stats() 显示 open 钉在上限、idle 为零、waitCount 数千且等待时长分钟级——连接一去不回;数据库侧会话长期滞留在 idle in transaction,与打开未关的 rows 迭代器一一镜像。goroutine 转储补全画面:大量栈停在 database/sql.(*DB).conn 等待空闲连接。此前正常的查询开始超时,是“服务跑一段时间后”才出现的延迟引爆特征。

诊断思路

  先看 db.Stats() 的 WaitCount/WaitDuration 量化排队,再对照 pg_stat_activity 的状态分布找 idle in transaction 会话;然后审计所有 db.Query/tx.Query 调用点,附近没有 defer rows.Close 的就是泄漏者;最后实时观察池指标,OpenConnections 爬到 MaxOpenConnections 后停滞而 WaitCount 持续增长,就是耗尽签名。返回 *sql.Rows 的辅助函数把关闭义务散播给几十个调用点,是结构性放大器。

根因

  每个 *sql.Rows 在 Close 之前独占一条连接。未关闭的 rows 永久把连接移出池子;稳定流量下池子到达 MaxOpenConnections,后续每个查询都在排队,直到上下文取消或超时。

修复与验证

  Query 之后立即 defer rows.Close(),每条退出路径(含 rows.Next 循环内的错误提前返回)都会释放连接;重构返回 rows 的辅助函数——在函数内消费完毕返回值,调用者拿到的不再是资源而是数据;池上限作为审慎决策设定——MaxOpenConns 匹配数据库容量、ConnMaxLifetime 适配负载均衡与防火墙的空闲切断。验证时驱动固定查询负载,断言结束后 OpenConnections 回落到 MaxIdleConns、WaitCount 保持为零。只调大 MaxOpenConnections 而不修泄漏,等于把耗尽转嫁给与其他服务共享的数据库全局上限。

代码示例

sql: connection pool exhausted
stats := db.Stats()
log.Printf("open=%d idle=%d waitCount=%d waitDuration=%s",
    stats.OpenConnections, stats.Idle, stats.WaitCount, stats.WaitDuration)
// open=50 idle=0 waitCount=1421 waitDuration=14m
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
    return err
}
defer rows.Close() // releases the connection on every exit path

for rows.Next() {
    if err := rows.Scan(&id, &name); err != nil {
        return err // deferred close still runs
    }
    // process
}
return rows.Err()
// Consume the rows here; callers receive values, not resources.
func ListUsers(ctx context.Context) ([]User, error) {
    rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var out []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name); err != nil {
            return nil, err
        }
        out = append(out, u)
    }
    return out, rows.Err()
}