Agrupación de conexiones con go-mssqldb

El go-mssqldb controlador utiliza el pool de conexiones integrado que proporciona el paquete de database/sql Go. Cada sql.DB instancia mantiene un conjunto de conexiones inactivas que se reutilizan automáticamente. Este artículo explica cómo configurar el pool para tu carga de trabajo.

Cómo funciona la piscina

Cuando llamas a db.QueryContext, db.ExecContext o a cualquier otro método de la base de datos:

  1. La piscina intenta encontrar una conexión inactiva.
  2. Si no hay conexión inactiva y el pool no ha alcanzado su tamaño máximo, se crea una nueva conexión.
  3. Si el grupo está a su capacidad máxima, la llamada se bloquea hasta que haya una conexión disponible.
  4. Una vez finalizada la operación, la conexión se devuelve a la piscina.

Métodos de configuración de pool

Configura el pool usando métodos en *sql.DB:

Método Description
db.SetMaxOpenConns(n) Número máximo de conexiones abiertas (en uso + inactividad). Predeterminado: 0 (ilimitado).
db.SetMaxIdleConns(n) Número máximo de conexiones en reposo en la piscina. Valor predeterminado: 2.
db.SetConnMaxLifetime(d) Tiempo total máximo que una conexión puede reutilizarse. Por defecto: 0 (sin límite).
db.SetConnMaxIdleTime(d) Tiempo máximo que una conexión puede estar inactiva antes de cerrarse. Por defecto: 0 (sin límite).

Example

Configura el pool inmediatamente después de abrir la base de datos:

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)

Trata estos valores como un punto de partida, no como un valor predeterminado universal. Para muchos servicios, el primer paso útil es establecer límites MaxOpenConns y MaxIdleConns, y luego añadir límites de vida útil y de inactividad solo cuando tu ruta de despliegue pueda dejarte con conexiones estancadas o distribuidas de forma desigual.

Escenario MaxOpen MaxIdle MaxLifetime MaxIdleTime
Aplicación web en una ruta estable de red de SQL Server 25 10 0 0
Aplicación web a través de Azure SQL, una pasarela o un balanceador de carga 25 10 5 minutos 1 minuto
Servicio de alto rendimiento 50-100 25 5 minutos 30 segundos
Trabajo de fondo / herramienta CLI 5 2 0 0
Azure SQL Database (Basic/Standard) 10-20 5 5 minutos 1 minuto

Tip

Establece MaxOpenConns por debajo del límite de conexión de tu instancia de SQL Server o del nivel Azure SQL. Superar el máximo de conexiones concurrentes del servidor provoca fallos de inicio de sesión para todos los clientes.

Los valores ConnMaxLifetime y ConnMaxIdleTime cortos reducen la probabilidad de conexiones obsoletas tras una conmutación por error o el reciclaje de la puerta de enlace, pero también aumentan la rotación de conexiones. Si tu aplicación se conecta directamente a una instancia estable de SQL Server y no ves fallos de conexión obsoleta, dejar ambos valores en 0 es razonable.

Supervisión de las estadísticas del grupo

Usa db.Stats() para leer las estadísticas actuales del pool:

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)

Campos clave:

Campo Description
OpenConnections Conexiones abiertas totales (en uso + inactividad).
InUse Conexiones que actualmente han revisado los llamantes.
Idle Conexiones esperando en la piscina.
WaitCount Número total de veces que un llamante tuvo que esperar una conexión.
WaitDuration Tiempo total acumulado de espera.

Si WaitCount crece de forma constante, no aumentes MaxOpenConns automáticamente. Primero verifica que las filas, transacciones y conexiones dedicadas se estén cerrando rápidamente y confirma que el servidor puede soportar un pool más grande.

SessionInitSQL

Úsalo SessionInitSQL para ejecutar una instrucción SQL en cada nueva conexión a medida que entra en el pool. Esta función es útil para establecer opciones a nivel de sesión:

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)

Anclaje de conexión

Ciertas operaciones fijan una conexión para que no se devuelva al pool hasta que la operación termina:

  • Transacciones (db.BeginTx) - La conexión queda fijada hasta que se llame a Commit() o Rollback().
  • Conexiones individuales (db.Conn) - La conexión queda fijada hasta que se llame a conn.Close().
  • Filas abiertas (db.QueryContext) - La conexión queda bloqueada hasta que se llama a rows.Close().

Cierra siempre estos recursos de inmediato para evitar agotar el pool.

Detectar y resolver el agotamiento de la piscina

El agotamiento de la piscina ocurre cuando todas las conexiones están en uso y la piscina ha alcanzado MaxOpenConns. Los nuevos llamantes bloquean hasta que se devuelva la conexión. Los síntomas incluyen alta latencia, acumulación de gorutinas y errores eventuales en la fecha límite del contexto.

Monitorizar el agotamiento

Consulta db.Stats() periódicamente y genera una alerta cuando se detecte contención:

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)
            }
        }
    }
}

Causas habituales y sus soluciones

Causa Síntoma Solución
MaxOpenConns demasiado baja para la carga de trabajo WaitCount crece de forma constante. Aumente MaxOpenConns.
Filas sin cerrar en rutas de error InUse crece, Idle permanece en 0. Use defer rows.Close() inmediatamente después de QueryContext.
Transacciones de larga duración InUse se mantiene alto. Mantén las transacciones cortas. Usa tiempos de espera contextuales.
db.Conn usado innecesariamente InUse más alto de lo esperado. Usa db.Conn solo cuando necesites estado con ámbito de sesión (tablas temporales).
MaxOpenConns no establecido (ilimitado) Cientos de conexiones abiertas bajo carga. Establezca siempre MaxOpenConns en un valor acotado.

Chequeos de salud y detección de conexiones obsoletas

El pool no valida activamente las conexiones inactivas. Una conexión que se quedó inactiva mientras el servidor la reciclaba falla en el siguiente uso. Configura ConnMaxLifetime y ConnMaxIdleTime para rotar las conexiones antes de que se vuelvan obsoletas:

// 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)

Nota:

Cuando ConnMaxIdleTime cierra conexiones inactivas de forma proactiva, los registros de SQL Server pueden mostrar que se han cortado conexiones. Esto es un comportamiento esperado, no una fuga de conexión. Si tu DBA informa de cortes inesperados de conexión, verifica que la ConnMaxIdleTime configuración coincida con las expectativas de monitorización del equipo.

Si tu aplicación se conecta a través de un balanceador de carga o de Azure SQL con replicación geográfica, establece ConnMaxLifetime en 5 minutos como máximo. Esta configuración garantiza que las conexiones se redistribuyan entre réplicas tras un conmutamiento por error.

Validar la conectividad al arrancar

Siempre llama db.PingContext después de abrir la base de datos para confirmar que la cadena de conexión es correcta y que el servidor es accesible:

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)
}

Exportar métricas del grupo

Expón las estadísticas del grupo en tu sistema de monitorización leyendo periódicamente db.Stats():

Ejemplo de Prometeo

Registra medidores que registran las estadísticas de la piscina y actualízalos periódicamente:

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
        }
    }
}

Lista de comprobación de la configuración de la agrupación

Area Recommendation
MaxOpenConns Siempre fijado en un valor acotado. Ajústalo a la concurrencia de tu carga de trabajo, por debajo del límite de conexiones del servidor.
MaxIdleConns Ajústelo como mínimo a la mitad de MaxOpenConns. Muy pocas conexiones en reposo provocan una sobrecarga debida a las reconexiones frecuentes.
ConnMaxLifetime Establece 5 minutos para Azure SQL o entornos con equilibrio de carga. Evita la acumulación de conexiones obsoletas.
ConnMaxIdleTime Configura entre 30 y 60 segundos para cerrar conexiones que ya no son necesarias.
Monitorización Sondee db.Stats() y alerte sobre el aumento de WaitCount.
Limpieza de recursos Siempre defer rows.Close(), defer tx.Rollback(), y defer conn.Close().
Validación de arranque Llama db.PingContext después sql.Open para verificar la conectividad.