使用 go-mssqldb 实现连接池

go-mssqldb驱动程序使用Godatabase/sql包内置的连接池。 每个 sql.DB 实例都维护一个空闲连接池,这些连接会自动被重复使用。 本文将解释如何配置你的工作负载池。

泳池的工作原理

当你调用 db.QueryContextdb.ExecContext或任何其他数据库方法时:

  1. 池子试图找到空闲连接。
  2. 如果没有空闲连接且池未达到最大容量,则会创建一个新的连接。
  3. 如果池已满载,呼叫会被阻塞,直到连接可用。
  4. 操作完成后,连接会返回到池中。

池配置方法

使用 *sql.DB 上的方法配置池:

方法 Description
db.SetMaxOpenConns(n) 最大打开连接数(使用中 + 空闲)。 默认值: 0 (无限)。
db.SetMaxIdleConns(n) 池中空闲连接的最大数量。 默认值:2
db.SetConnMaxLifetime(d) 连接可重复使用的最大总时间。 默认值: 0 (无限制)。
db.SetConnMaxIdleTime(d) 连接在被关闭前可保持空闲的最长时间。 默认值: 0 (无限制)。

Example

打开数据库后立即配置池:

db, err := sql.Open("sqlserver", connString)
if err != nil {
    log.Fatal(err)
}

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)

把这些数值当作起点,而不是普遍默认值。 对于许多服务来说,第一步有用的是设置有界 MaxOpenConnsMaxIdleConns,然后只有在部署路径可能导致连接过时或分布不均时,才添加寿命和空闲限制。

情景 MaxOpen 最大空闲 MaxLifetime 最大空闲时间
稳定SQL Server网络路径上的Web应用 25 10 0 0
通过Azure SQL、网关或负载均衡器的网页应用 25 10 5 分钟 1 分钟
高通量服务 50-100 25 5 分钟 30 秒
后台作业 / 命令行工具 5 2 0 0
Azure SQL 数据库 (Basic/Standard) 10-20 5 5 分钟 1 分钟

Tip

MaxOpenConns设置为低于 SQL Server 实例或 Azure SQL 服务层级的连接限制。 超过服务器最大并发连接数会导致所有客户端登录失败。

较短的 ConnMaxLifetimeConnMaxIdleTime 值可以降低在故障切换或网关重启后出现陈旧连接的可能性,但也会增加连接波动。 如果你的应用直接连接到一个稳定的 SQL Server 实例,并且没有出现陈旧连接导致的失败,那么将这两个值都保持为 0 是合理的。

监控泳池统计

使用 db.Stats() 读取当前池统计信息:

stats := db.Stats()
fmt.Printf("Open: %d, InUse: %d, Idle: %d\n",
    stats.OpenConnections, stats.InUse, stats.Idle)
fmt.Printf("WaitCount: %d, WaitDuration: %v\n",
    stats.WaitCount, stats.WaitDuration)

关键字段:

领域 Description
OpenConnections 已打开的连接总数(使用中 + 空闲)。
InUse 当前由调用方签出的连接。
Idle 泳池里等待着联系。
WaitCount 来电者等待连接的总次数。
WaitDuration 累计等待时间。

如果 WaitCount 稳步增长,不要自动增加 MaxOpenConns。 首先确认行、事务和专用连接是否能及时关闭,并确认服务器能够支持更大的池。

SessionInitSQL

使用 SessionInitSQL 在每个新连接进入连接池时运行一条 SQL 语句。 此功能对于设置会话级别选项非常有用:

import (
    "database/sql"
    "github.com/microsoft/go-mssqldb"
    "github.com/microsoft/go-mssqldb/msdsn"
)

config := msdsn.Config{
    Host:     "<server>",
    Port:     1433,
    Database: "AdventureWorks2025",
}

connector := mssql.NewConnectorConfig(config)
connector.SessionInitSQL = "SET ANSI_NULLS ON; SET QUOTED_IDENTIFIER ON"
db := sql.OpenDB(connector)

连接固定

某些操作会钉住连接,使其在操作结束前不会返回到池中:

  • 事务db.BeginTx)- 连接会一直保持固定状态,直到调用 Commit()Rollback()
  • 单个连接 (db.Conn) - 该连接会被固定,直到调用 conn.Close()
  • 打开的行 (db.QueryContext) - 连接会一直保持固定,直到调用 rows.Close()

务必及时关闭这些资源,以免资源池耗尽。

检测并解决池耗尽

当所有连接都已使用且池子达到 MaxOpenConns时,池子耗尽就会发生。 新的调用方会被阻塞,直到有连接返回。 症状包括高延迟、流程积累以及最终的上下文截止日期错误。

监测疲劳情况

定期进行轮询 db.Stats() ,并在检测到争议时提醒:

func monitorPool(ctx context.Context, db *sql.DB, interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    var lastWaitCount int64
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            stats := db.Stats()
            newWaits := stats.WaitCount - lastWaitCount
            lastWaitCount = stats.WaitCount

            if newWaits > 0 {
                log.Printf("POOL CONTENTION: %d new waits, avg wait %v, open=%d, inUse=%d, idle=%d",
                    newWaits, stats.WaitDuration/time.Duration(stats.WaitCount),
                    stats.OpenConnections, stats.InUse, stats.Idle)
            }
        }
    }
}

常见原因和解决方案

原因 症状 解决方案
MaxOpenConns 对于工作负载来说太低了 WaitCount 稳步增长。 增大 MaxOpenConns
错误路径中未封闭的行 InUse 增长,Idle 保持为 0。 紧接在QueryContext之后使用defer rows.Close()
长时间运行的事务 InUse 维持高位。 保持交易简短。 使用上下文超时机制。
db.Conn 被不必要地使用 InUse 比预期还要高。 仅在需要会话范围内的状态(例如临时表)时,才使用 db.Conn
MaxOpenConns 未设定(无限) 在负载下有数百个打开的连接。 始终将 MaxOpenConns 设置为有界值。

健康检查与失效连接检测

连接池不会主动验证空闲连接。 服务器循环时处于空闲状态的连接,下次使用时会失败。 配置 ConnMaxLifetimeConnMaxIdleTime,以便在连接失效前轮换连接:

// Rotate connections every 5 minutes to stay compatible
// with load balancers and Azure SQL failover.
db.SetConnMaxLifetime(5 * time.Minute)

// Close connections that have been idle for over 1 minute
// to reduce the number of stale connections.
db.SetConnMaxIdleTime(1 * time.Minute)

注释

ConnMaxIdleTime主动关闭空闲连接时,SQL Server 日志可能会显示连接被丢弃。 这是预期中的行为,不是连接泄漏。 如果您的DBA报告了意外的连接关闭,请确认设置 ConnMaxIdleTime 是否符合团队的监控预期。

如果你的应用通过负载均衡器或启用了异地复制的 Azure SQL 进行连接,请将 ConnMaxLifetime 设置为 5 分钟或更短。 该设置可确保在故障切换后,连接会在各个副本之间重新分配。

启动时验证连接性

打开数据库后,务必呼叫db.PingContext确认连接字符串正确且服务器可达:

db, err := sql.Open("sqlserver", connString)
if err != nil {
    log.Fatal(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
    log.Fatalf("Cannot connect to database: %v", err)
}

出口池指标

通过定期阅读 db.Stats(),将泳池统计数据暴露给你的监控系统:

普罗米修斯的例子

注册用于跟踪连接池统计信息并定期更新这些信息的仪表指标:

import "github.com/prometheus/client_golang/prometheus"

var (
    dbOpenConns = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "db_open_connections",
        Help: "Number of open database connections.",
    })
    dbInUseConns = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "db_in_use_connections",
        Help: "Number of connections currently in use.",
    })
    dbWaitCount = prometheus.NewCounter(prometheus.CounterOpts{
        Name: "db_wait_count_total",
        Help: "Total number of times a caller waited for a connection.",
    })
    dbWaitDuration = prometheus.NewCounter(prometheus.CounterOpts{
        Name: "db_wait_duration_seconds_total",
        Help: "Total wait time for a connection.",
    })
)

func init() {
    prometheus.MustRegister(dbOpenConns, dbInUseConns, dbWaitCount, dbWaitDuration)
}

func recordPoolMetrics(ctx context.Context, db *sql.DB) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    var lastWaitCount int64
    var lastWaitDuration time.Duration
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            stats := db.Stats()
            dbOpenConns.Set(float64(stats.OpenConnections))
            dbInUseConns.Set(float64(stats.InUse))
            dbWaitCount.Add(float64(stats.WaitCount - lastWaitCount))
            dbWaitDuration.Add((stats.WaitDuration - lastWaitDuration).Seconds())
            lastWaitCount = stats.WaitCount
            lastWaitDuration = stats.WaitDuration
        }
    }
}

池配置检查清单

Area Recommendation
MaxOpenConns 始终设置为有界值。 将其与工作负载的并发度相匹配,并确保低于服务器的连接上限。
MaxIdleConns 设置为至少是MaxOpenConns的一半。 空闲连接过少会导致频繁重连,从而产生额外开销。
ConnMaxLifetime 对于 Azure SQL 或负载均衡环境,请设置为 5 分钟。 防止失效连接堆积。
ConnMaxIdleTime 将其设置为 30-60 秒,以关闭不再需要的连接。
监测 轮询 db.Stats() 并对 WaitCount 的增长发出警报。
资源清理 总是 defer rows.Close()defer tx.Rollback()defer conn.Close()
启动验证 sql.Open 之后调用 db.PingContext 以验证连通性。