Pooling di connessioni con go-mssqldb

Il go-mssqldb driver utilizza il pool di connessione integrato fornito dal pacchetto di database/sql Go. Ogni sql.DB istanza mantiene un pool di connessioni inattive che vengono riutilizzate automaticamente. Questo articolo spiega come configurare il pool per il tuo carico di lavoro.

Come funziona la piscina

Quando chiami db.QueryContext, db.ExecContext, o qualsiasi altro metodo di database:

  1. La piscina cerca di trovare una connessione inattiva.
  2. Se non è disponibile una connessione inattiva e il pool non ha raggiunto la sua dimensione massima, viene creata una nuova connessione.
  3. Se il pool è alla massima capacità, la chiamata si blocca finché non diventa disponibile una connessione.
  4. Dopo il completamento dell'operazione, la connessione viene restituita al pool.

Metodi di configurazione del pool

Configura il pool usando metodi su *sql.DB:

metodo Description
db.SetMaxOpenConns(n) Numero massimo di connessioni aperte (in uso + inattività). Predefinito: 0 (illimitato).
db.SetMaxIdleConns(n) Numero massimo di connessioni inattive nella piscina. Impostazione predefinita: 2.
db.SetConnMaxLifetime(d) Tempo totale massimo di riutilizzo di una connessione. Predefinito: 0 (nessun limite).
db.SetConnMaxIdleTime(d) Il tempo massimo in cui una connessione può rimanere inattiva prima di essere chiusa. Predefinito: 0 (nessun limite).

Example

Configura il pool immediatamente dopo aver aperto il database:

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)

Considera questi valori come un punto di partenza, non come un predefinito universale. Per molti servizi, il primo passo utile è impostare limiti limitati MaxOpenConns e MaxIdleConns, poi aggiungere limiti di durata e inattività solo quando il percorso di distribuzione può lasciarti con connessioni obsolete o distribuite in modo disomogeneo.

Scenario MaxOpen MaxIdle MaxLifetime MaxIdleTime
Applicazione web su un percorso stabile di rete SQL Server 25 10 0 0
Applicazione web tramite Azure SQL, un gateway o un bilanciatore di carico 25 10 5 minuti 1 minuto
Servizio ad alta produttività 50-100 25 5 minuti 30 secondi
Lavoro di background / Strumento CLI 5 2 0 0
database SQL di Azure (Basic/Standard) 10-20 5 5 minuti 1 minuto

Tip

Imposta MaxOpenConns sotto il limite di connessione della tua istanza SQL Server o del tier Azure SQL. Superare il limite massimo di connessioni concorrenti del server causa fallimenti di accesso per tutti i client.

Valori brevi per ConnMaxLifetime e ConnMaxIdleTime riducono la probabilità di connessioni non più valide dopo il failover o il riciclo del gateway, ma aumentano anche il ricambio delle connessioni. Se la tua app si connette direttamente a un'istanza stabile di SQL Server e non riscontri errori dovuti a connessioni non più valide, è ragionevole lasciare entrambi i valori su 0.

Monitorare le statistiche del pool

Da usare db.Stats() per leggere le statistiche attuali 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)

Campi chiave:

Campo Description
OpenConnections Totale delle connessioni aperte (in uso + inattività).
InUse Collegamenti attualmente controllati dai chiamanti.
Idle Connessioni in attesa nel pool.
WaitCount Numero totale di volte in cui un chiamante ha dovuto aspettare una connessione.
WaitDuration Tempo totale cumulativo di attesa.

Se WaitCount sta crescendo costantemente, non aumentare MaxOpenConns automaticamente. Prima verifica che righe, transazioni e connessioni dedicate vengano chiuse prontamente e conferma che il server possa supportare un pool più ampio.

SessionInitSQL

Usalo SessionInitSQL per eseguire un'istruzione SQL su ogni nuova connessione man mano che entra nel pool. Questa funzione è utile per impostare le opzioni a livello di sessione:

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)

Blocco della connessione

Alcune operazioni vincolano una connessione in modo che non venga restituita al pool finché l'operazione non termina:

  • Transazioni (db.BeginTx) - La connessione è vincolata finché Commit() o Rollback() non viene chiamata.
  • Connessioni singole (db.Conn) - La connessione rimane associata finché conn.Close() non viene chiamato.
  • Righe aperte (db.QueryContext) - La connessione rimane bloccata finché non viene chiamato rows.Close().

Chiudi sempre tempestivamente queste risorse per evitare di esaurire il pool.

Rilevare e risolvere l'esaurimento del pool di risorse

L'esaurimento della piscina si verifica quando tutte le connessioni sono in uso e la piscina ha raggiunto MaxOpenConns. I nuovi chiamanti bloccano finché non viene restituita una connessione. I sintomi includono alta latenza, accumulo di goroutine e eventuali errori di scadenza contestuale.

Monitorare la stanchezza

Interroga db.Stats() periodicamente e avvisa quando viene rilevata una contesa:

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

Cause comuni e soluzioni

Cause Sintomo Soluzione
MaxOpenConns troppo basso per il carico di lavoro WaitCount cresce costantemente. Aumentare MaxOpenConns.
Righe non chiuse nei percorsi di errore InUse cresce, Idle rimane a 0. Usa defer rows.Close() immediatamente dopo QueryContext.
Transazioni a esecuzione prolungata InUse rimane alto. Mantieni le transazioni brevi. Usa i timeout di contesto.
db.Conn usata inutilmente InUse più alto del previsto. Usa db.Conn solo quando hai bisogno di uno stato limitato all'ambito della sessione (tabelle temporanee).
MaxOpenConns non impostato (illimitato) Centinaia di connessioni aperte sotto carico. Imposta MaxOpenConns sempre su un valore limitato.

Controlli di salute e rilevamento delle connessioni obsolete

Il pool non convalida attivamente le connessioni inattive. Una connessione rimasta inattiva mentre il server la riciclava si guasta al prossimo utilizzo. Configurare ConnMaxLifetime e ConnMaxIdleTime per ruotare le connessioni prima che diventino obsolete:

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

Note

Quando ConnMaxIdleTime chiude le connessioni inattive in modo proattivo, i log di SQL Server potrebbero mostrare la perdita delle connessioni. Questo è un comportamento previsto, non una perdita di connessione. Se il tuo DBA segnala chiusure di connessione impreviste, verifica che l'impostazione ConnMaxIdleTime sia in linea con le aspettative di monitoraggio del team.

Se la tua applicazione si collega tramite un bilanciatore di carico o Azure SQL con geo-replicazione, imposta ConnMaxLifetime a 5 minuti o meno. Questa impostazione garantisce che le connessioni vengano ridistribuite tra le repliche dopo un failover.

Valida la connettività all'avvio

Chiama db.PingContext sempre dopo aver aperto il database per confermare che la stringa di connessione è corretta e che il server è raggiungibile:

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

Metriche del pool di esportazione

Esponi le statistiche dei pool al tuo sistema di monitoraggio leggendo db.Stats()periodicamente:

Esempio di Prometheus

Registra gli indicatori che tracciano le statistiche del pool e aggiornali periodicamente:

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

Elenco di controllo per la configurazione del pool

Area Raccomandazione
MaxOpenConns Imposta sempre un valore delimitato. Impostalo in base al livello di concorrenza del tuo carico di lavoro, al di sotto del limite di connessioni del server.
MaxIdleConns Impostato ad almeno metà di MaxOpenConns. Un numero troppo basso di connessioni inattive comporta frequenti sovraccarichi dovuti alla riconnessione.
ConnMaxLifetime Impostare su 5 minuti per Azure SQL o per ambienti con bilanciamento del carico. Previene l'accumulo di connessioni stantie.
ConnMaxIdleTime Imposta su 30-60 secondi per chiudere le connessioni che non sono più necessarie.
Monitoring Sondaggi db.Stats() e avvisi sulla WaitCount crescita.
Pulizia delle risorse Sempre defer rows.Close(), defer tx.Rollback(), e defer conn.Close().
Validazione dell'avvio Chiama db.PingContext dopo sql.Open per verificare la connettività.