Pula połączeń w go-mssqldb

Sterownik go-mssqldb korzysta z wbudowanej puli połączeń udostępnianej przez pakiet database/sql języka Go. Każda sql.DB instancja utrzymuje pulę bezczynnych połączeń, które są automatycznie wykorzystywane. Ten artykuł wyjaśnia, jak skonfigurować pulę pod kątem obciążenia.

Jak działa basen

Gdy wywołasz db.QueryContext, db.ExecContext, lub dowolną inną metodę bazy danych:

  1. Basen próbuje znaleźć bezczynne połączenie.
  2. Jeśli nie ma dostępnego bezczynnego połączenia, a pula nie osiągnęła jeszcze maksymalnego rozmiaru, tworzone jest nowe połączenie.
  3. Jeśli pula osiągnęła maksymalną pojemność, wywołanie jest blokowane do momentu, gdy połączenie stanie się dostępne.
  4. Po zakończeniu operacji połączenie wraca do puli.

Metody konfiguracji puli

Skonfiguruj pulę za pomocą metod dostępnych w elemencie *sql.DB:

Metoda Description
db.SetMaxOpenConns(n) Maksymalna liczba otwartych połączeń (w użyciu + bezczynność). Domyślne: 0 (nieograniczone).
db.SetMaxIdleConns(n) Maksymalna liczba bezczynnych połączeń w puli. Wartość domyślna: 2.
db.SetConnMaxLifetime(d) Maksymalny czas ponownego użycia połączenia. Domyślne: 0 (bez limitu).
db.SetConnMaxIdleTime(d) Maksymalny czas, przez jaki połączenie może stać nieczynne, zanim zostanie zamknięte. Domyślne: 0 (bez limitu).

Example

Skonfiguruj pulę natychmiast po otwarciu bazy danych:

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)

Traktuj te wartości jako punkt wyjścia, a nie uniwersalny domyślny punkt wyjścia. Dla wielu usług pierwszym użytecznym krokiem jest ustawienie limitów dla MaxOpenConns i MaxIdleConns, a następnie dodanie limitów czasu życia i bezczynności tylko wtedy, gdy sposób wdrożenia może prowadzić do nieaktualnych lub nierównomiernie rozłożonych połączeń.

Scenario MaxOpen MaxIdle MaxLifetime MaxIdleTime
Aplikacja webowa na stabilnej ścieżce sieci SQL Server 25 10 0 0
Aplikacja internetowa za pośrednictwem Azure SQL, bramy lub modułu równoważenia obciążenia 25 10 5 minut 1 minuta
Usługi o wysokiej przepustowości 50-100 25 5 minut 30 sekund
Zadanie w tle / narzędzie CLI 5 2 0 0
Azure SQL Database (Basic/Standard) 10-20 5 5 minut 1 minuta

Wskazówka

Ustaw MaxOpenConns poniżej limitu połączeń dla instancji SQL Server lub warstwy Azure SQL. Przekroczenie maksymalnej liczby jednoczesnych połączeń serwera powoduje awarie logowania dla wszystkich klientów.

Krótkie wartości ConnMaxLifetime i ConnMaxIdleTime zmniejszają ryzyko nieaktualnych połączeń po przełączeniu awaryjnym lub ponownym uruchomieniu bramy, ale jednocześnie zwiększają częstotliwość odnawiania połączeń. Jeśli Twoja aplikacja łączy się bezpośrednio ze stabilną instancją SQL Server i nie występują błędy związane z nieaktualnymi połączeniami, rozsądne jest pozostawienie obu wartości na 0.

Monitorowanie statystyk puli

Użyj db.Stats(), aby odczytać aktualne statystyki puli:

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)

Pola klucza:

Pole Description
OpenConnections Całkowite otwarte połączenia (w użyciu + bezczynność).
InUse Połączenia obecnie sprawdzane przez dzwoniących.
Idle Połączenia oczekujące w puli.
WaitCount Łączna liczba razy, gdy dzwoniący musiał czekać na połączenie.
WaitDuration Łączny czas oczekiwania łącznie.

Jeśli WaitCount stale rośnie, nie zwiększaj automatycznie MaxOpenConns. Najpierw sprawdź, czy wiersze, transakcje i dedykowane połączenia są szybko zamykane oraz potwierdz, że serwer może obsłużyć większą pulę.

SessionInitSQL

Użyj SessionInitSQL, aby uruchomić instrukcję SQL dla każdego nowego połączenia w momencie dodawania go do puli. Ta funkcja jest przydatna do ustawiania opcji na poziomie sesji:

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)

Przypinanie połączenia

Niektóre operacje blokują połączenie, tak aby nie zostało ono zwrócone do puli aż do zakończenia operacji:

  • Transakcje (db.BeginTx) — Połączenie jest przypisane do czasu wywołania Commit() lub Rollback().
  • Pojedyncze połączenie (db.Conn) — Połączenie pozostaje przypisane aż do wywołania conn.Close().
  • Otwarte wiersze (db.QueryContext) — połączenie pozostaje przypięte do momentu wywołania rows.Close().

Aby nie wyczerpać puli, zawsze niezwłocznie zamykaj te zasoby.

Wykryć i rozwiązać wyczerpanie puli

Wyczerpanie puli połączeń występuje, gdy wszystkie połączenia są używane, a pula osiągnęła MaxOpenConns. Nowi dzwoniący blokują połączenie, dopóki nie zostanie odwrócone. Objawy obejmują wysokie opóźnienia, narastanie gorutyn oraz błędy terminów kontekstowych.

Monitor wyczerpania

Okresowo prowadź ankiety db.Stats() i bądź upozorniony, gdy wykryje się konkurencja:

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

Typowe przyczyny i rozwiązania

Przyczyna Objaw Rozwiązanie
MaxOpenConns zbyt niskie dla obciążenia roboczego WaitCount stale rośnie. Zwiększ wartość MaxOpenConns.
Niezamknięte wiersze w ścieżkach błędów InUse rośnie, Idle pozostaje na poziomie 0. Użyj defer rows.Close() zaraz po QueryContext.
Długotrwałe transakcje InUse Utrzymuje się wysoko. Utrzymuj transakcje krótkie. Używaj czasowych limitów kontekstowych.
db.Conn Używana niepotrzebnie InUse Wyższy niż się spodziewałem. Używaj db.Conn tylko wtedy, gdy potrzebujesz stanu ograniczonego do sesji (tabele tymczasowe).
MaxOpenConns nie ustawiono (bez ograniczeń) Setki otwartych połączeń pod obciążeniem. Zawsze ustawiaj MaxOpenConns na ograniczoną wartość.

Kontrole stanu i wykrywanie przestarzałych połączeń

Pula nie weryfikuje aktywnie bezczynnych połączeń. Połączenie, które leżało nieaktywnie podczas recyklingu serwera, zawodzi przy następnym użyciu. Konfiguruj ConnMaxLifetime i ConnMaxIdleTime rotuj połączenia, zanim staną się nieaktualne:

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

Uwaga / Notatka

Gdy ConnMaxIdleTime proaktywnie zamyka bezczynne połączenia, w dziennikach SQL Server może być widoczne ich rozłączanie. To jest oczekiwane zachowanie, a nie wyciek połączeń. Jeśli Twój DBA zgłasza nieoczekiwane zamknięcia połączenia, sprawdź, czy ustawienie ConnMaxIdleTime jest zgodne z oczekiwaniami zespołu dotyczącymi monitoringu.

Jeśli aplikacja łączy się za pośrednictwem modułu równoważenia obciążenia lub usługi Azure SQL z georeplikacją, ustaw wartość ConnMaxLifetime na 5 minut lub mniej. To ustawienie zapewnia redystrybucję połączeń pomiędzy replikami po przełączeniu awaryjnym.

Weryfikacja łączności przy starcie

Zawsze dzwoń db.PingContext po otwarciu bazy danych, aby potwierdzić, czy parametry połączenia jest poprawny i serwer jest dostępny:

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

Metryki puli eksportowej

Udostępnij statystyki puli swojemu systemowi monitorującemu, okresowo odczytując db.Stats():

Przykład Prometeusza

Rejestruj mierniki śledzące statystyki puli i okresowo je aktualizuj:

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 kontrolna konfiguracji puli

Area Zalecenie
MaxOpenConns Zawsze ustawiona na wartość ograniczoną. Dopasuj to do współbieżności obciążenia, poniżej limitu połączenia serwera.
MaxIdleConns Ustaw na co najmniej połowę MaxOpenConns. Zbyt mała liczba bezczynnych połączeń powoduje częste obciążenie związane z ponownym nawiązywaniem połączenia.
ConnMaxLifetime Ustaw na 5 minut dla środowisk Azure SQL lub środowisk z równoważeniem obciążenia. Zapobiega powstawaniu przestarzałych połączeń.
ConnMaxIdleTime Ustaw 30-60 sekund na zamknięcie połączeń, które już nie są potrzebne.
Nadzorowanie Monitoruj db.Stats() i otrzymuj alerty o wzroście WaitCount.
Oczyszczanie zasobów Zawsze defer rows.Close(), defer tx.Rollback(), oraz defer conn.Close().
Walidacja startu Zadzwoń db.PingContext po tym sql.Open , aby potwierdzić łączność.