Verbindungspooling mit go-mssqldb

Der Treiber go-mssqldb verwendet den integrierten Verbindungspool, der vom Go-Paket database/sql bereitgestellt wird. Jede Instanz sql.DB unterhält einen Pool von ungenutzten Verbindungen, die automatisch wiederverwendet werden. Dieser Artikel erklärt, wie Sie den Pool für Ihre Arbeitsbelastung konfigurieren können.

Wie das Schwimmbecken funktioniert

Wenn Sie db.QueryContext, db.ExecContext oder eine andere Datenbankmethode aufrufen:

  1. Der Pool versucht, eine Leerlaufverbindung zu finden.
  2. Wenn keine Leerlaufverbindung verfügbar ist und der Pool seine maximale Größe noch nicht erreicht hat, wird eine neue Verbindung erstellt.
  3. Wenn der Pool auf maximaler Kapazität ist, blockiert der Anruf, bis eine Verbindung verfügbar ist.
  4. Nach Abschluss der Operation wird die Verbindung zum Pool zurückgegeben.

Methoden der Poolkonfiguration

Konfigurieren Sie den Pool mit Methoden auf *sql.DB:

Methode Description
db.SetMaxOpenConns(n) Maximale Anzahl offener Verbindungen (im Einsatz + Leerlauf). Standard: 0 (unbegrenzt).
db.SetMaxIdleConns(n) Maximale Anzahl der ungenutzten Verbindungen im Pool. Standardwert: 2.
db.SetConnMaxLifetime(d) Die maximale Gesamtzeit einer Verbindung kann wiederverwendet werden. Standard: 0 (keine Begrenzung).
db.SetConnMaxIdleTime(d) Die maximale Zeit, in der eine Verbindung untätig bleiben kann, bevor sie geschlossen wird. Standard: 0 (keine Begrenzung).

Example

Konfigurieren Sie den Pool unmittelbar nach dem Öffnen der Datenbank:

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)

Betrachte diese Werte als Ausgangspunkt, nicht als universellen Standard. Für viele Dienste ist der erste sinnvolle Schritt, MaxOpenConns und MaxIdleConns zu begrenzen und Lebensdauer- und Leerlauflimits erst dann hinzuzufügen, wenn Ihr Bereitstellungspfad zu veralteten oder ungleich verteilten Verbindungen führen kann.

Szenario MaxOpen MaxIdle MaxLifetime MaxIdleTime
Webanwendung auf einem stabilen SQL Server-Netzwerkpfad 25 10 0 0
Webanwendung über Azure SQL, ein Gateway oder einen Load Balancer 25 10 5 Minuten 1 Minute
Dienst mit hohem Durchsatz 50-100 25 5 Minuten 30 Sekunden
Hintergrundjob / CLI-Tool 5 2 0 0
Azure SQL-Datenbank (Basic/Standard) 10 - 20 5 5 Minuten 1 Minute

Tip

Setzen Sie MaxOpenConns unter das Verbindungslimit Ihrer SQL Server-Instanz oder Azure SQL-Dienstebene. Das Überschreiten der maximalen gleichzeitigen Verbindungen des Servers führt zu Anmeldefehlern für alle Clienten.

Kurze ConnMaxLifetime- und ConnMaxIdleTime-Werte verringern die Wahrscheinlichkeit veralteter Verbindungen nach einem Failover oder Gateway-Recycling, erhöhen aber auch das häufige Neuaufbauen und Trennen von Verbindungen. Wenn deine App direkt eine Verbindung mit einer stabilen SQL Server-Instanz herstellt und keine Fehler aufgrund veralteter Verbindungen auftreten, ist es sinnvoll, beide Werte auf 0 zu belassen.

Pool-Statistiken überwachen

Verwenden Sie db.Stats(), um aktuelle Pool-Statistiken abzurufen:

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)

Schlüsselfelder:

Feld Description
OpenConnections Total offene Verbindungen (im Gebrauch + Leerlauf).
InUse Die Verbindungen werden derzeit von Anrufern überprüft.
Idle Im Pool wartende Verbindungen.
WaitCount Die Gesamtzahl der Male, in denen ein Anrufer auf eine Verbindung warten musste.
WaitDuration Gesamtkumulierte Wartezeit.

Wenn WaitCount stetig wächst, erhöhen Sie MaxOpenConns nicht automatisch. Überprüfen Sie zunächst, ob Zeilen, Transaktionen und dedizierte Verbindungen umgehend geschlossen werden, und bestätigen Sie, dass der Server einen größeren Pool unterstützen kann.

SessionInitSQL

Verwenden Sie SessionInitSQL, um bei jeder neuen Verbindung eine SQL-Anweisung auszuführen, wenn sie in den Pool aufgenommen wird. Diese Funktion ist nützlich, um Optionen auf Sitzungsebene einzustellen:

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)

Verbindungs-Pinning

Bestimmte Operationen binden eine Verbindung, sodass sie erst dann in den Pool zurückgegeben wird, wenn die Operation abgeschlossen ist:

  • Transaktionen (db.BeginTx) - Die Verbindung bleibt gebunden, bis Commit() oder Rollback() aufgerufen wird.
  • Einzelverbindungen (db.Conn) – Die Verbindung bleibt gepinnt, bis conn.Close() aufgerufen wird.
  • Offene Zeilen (db.QueryContext) – Die Verbindung bleibt gepinnt, bis rows.Close() aufgerufen wird.

Schließe diese Ressourcen immer umgehend, um den Pool nicht zu verhungern.

Pool-Erschöpfung erkennen und auflösen

Eine Pool-Erschöpfung tritt auf, wenn alle Verbindungen belegt sind und der Pool MaxOpenConns erreicht hat. Neue Anrufer blockieren, bis eine Verbindung zurückgerufen wird. Zu den Symptomen gehören eine hohe Latenz, eine Anhäufung von Goroutinen und schließlich Fehler aufgrund überschrittener Kontext-Deadlines.

Auf Erschöpfung überwachen

db.Stats() regelmäßig abfragen und bei Erkennung eines Konflikts warnen:

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

Häufige Ursachen und Lösungen

Ursache Symptom Lösung
MaxOpenConns Zu gering für die Arbeitsbelastung WaitCount wächst stetig. Erhöhe MaxOpenConns.
Zeilen, die in Fehlerpfaden nicht abgeschlossen sind InUse wächst, Idle bleibt bei 0. Verwenden defer rows.Close() Sie sofort nach QueryContext.
Lang andauernde Transaktionen InUse bleibt weiterhin hoch. Halten Sie die Transaktionen kurz. Nutze Kontext-Timeouts.
db.Conn unnötig verwendet InUse höher als erwartet. Verwende db.Conn nur, wenn du einen sitzungsbezogenen Status (temporäre Tabellen) benötigst.
MaxOpenConns nicht festgelegt (unbegrenzt) Hunderte offene Verbindungen unter Last. Setzen Sie MaxOpenConns immer auf einen begrenzten Wert.

Gesundheitsprüfungen und Erkennung von veralteten Verbindungen

Der Pool validiert inaktive Verbindungen nicht aktiv. Eine Verbindung, die untätig blieb, während der Server sie recycelte, schlägt bei der nächsten Nutzung fehl. Konfigurieren Sie ConnMaxLifetime und ConnMaxIdleTime so, dass Verbindungen rotiert werden, bevor sie inaktiv werden:

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

Hinweis

Wenn ConnMaxIdleTime inaktive Verbindungen proaktiv schließt, zeigen die SQL Server-Protokolle möglicherweise an, dass Verbindungen getrennt werden. Das ist erwartetes Verhalten, kein Verbindungsleck. Wenn Ihr DBA unerwartete Verbindungsschließungen meldet, überprüfen Sie, ob die Einstellung ConnMaxIdleTime mit den Überwachungserwartungen des Teams übereinstimmt.

Wenn Ihre Anwendung über einen Load Balancer oder Azure SQL mit Geo-Replikation eine Verbindung herstellt, setzen Sie ConnMaxLifetime auf 5 Minuten oder weniger. Diese Einstellung stellt sicher, dass Verbindungen nach einem Failover über Replikate verteilt werden.

Validiere die Konnektivität beim Start

Immer nach dem Öffnen der Datenbank aufrufendb.PingContext, um zu bestätigen, dass die Verbindungszeichenfolge korrekt ist und der Server erreichbar ist:

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

Exportpool-Metriken

Machen Sie Pool-Statistiken für Ihr Überwachungssystem verfügbar, indem Sie db.Stats() regelmäßig auslesen:

Beispiel Prometheus

Registriere Messgeräte, die Pool-Statistiken verfolgen, und aktualisieren sie regelmäßig:

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

Poolkonfigurations-Checkliste

Area Recommendation
MaxOpenConns Immer auf einen beschränkten Wert gesetzt. Passen Sie es an die Parallelität Ihrer Workloads an, unter dem Verbindungslimit des Servers.
MaxIdleConns Auf mindestens die Hälfte von MaxOpenConns setzen. Zu wenige Leerlaufverbindungen verursachen häufigen Wiederanschluss-Overhead.
ConnMaxLifetime Legen Sie für Azure SQL oder lastenausgeglichene Umgebungen 5 Minuten fest. Verhindert die Ansammlung veralteter Verbindungen.
ConnMaxIdleTime Stellen Sie auf 30-60 Sekunden, um Verbindungen zu schließen, die nicht mehr benötigt werden.
Monitoring Umfrage db.Stats() und Warnung zum WaitCount Wachstum.
Ressourcenbereinigung Immer defer rows.Close(), defer tx.Rollback(), und defer conn.Close().
Startbestätigung Rufen Sie db.PingContext nach sql.Open auf, um die Konnektivität zu überprüfen.