Agrupamento de ligações com go-mssqldb

O controlador go-mssqldb utiliza o pool de ligações integrado fornecido pelo pacote database/sql do Go. Cada sql.DB instância mantém um conjunto de ligações inativas que são reutilizadas automaticamente. Este artigo explica como configurar o pool para a sua carga de trabalho.

Como funciona a piscina

Quando chama db.QueryContext, db.ExecContext ou qualquer outro método de base de dados:

  1. A piscina tenta encontrar uma ligação ociosa.
  2. Se não houver ligação ociosa e o pool não tiver atingido o seu tamanho máximo, é criada uma nova ligação.
  3. Se o conjunto estiver na capacidade máxima, a chamada fica bloqueada até que uma conexão fique disponível.
  4. Após a conclusão da operação, a ligação é devolvida ao pool.

Métodos de configuração do pool

Configure o pool usando métodos em *sql.DB:

Método Description
db.SetMaxOpenConns(n) Número máximo de ligações abertas (em uso + inativa). Predefinição: 0 (ilimitado).
db.SetMaxIdleConns(n) Número máximo de ligações ociosas na piscina. Padrão: 2.
db.SetConnMaxLifetime(d) Tempo total máximo que uma ligação pode ser reutilizada. Padrão: 0 (sem limite).
db.SetConnMaxIdleTime(d) Tempo máximo em que uma ligação pode ficar parada antes de ser fechada. Padrão: 0 (sem limite).

Exemplo

Configure o pool imediatamente após abrir a base de dados:

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)

Trate estes valores como ponto de partida, não como um padrão universal. Para muitos serviços, o primeiro passo útil é definir limites limitados MaxOpenConns e MaxIdleConns, e depois adicionar limites de vida útil e de inatividade apenas quando o seu caminho de implementação pode deixá-lo com ligações estagnadas ou distribuídas de forma desigual.

Scenario MaxOpen MaxIdle MaxLifetime MaxIdleTime
Aplicação web num caminho de rede estável do SQL Server 25 10 0 0
Aplicação web através do SQL do Azure, um gateway ou um balanceador de carga 25 10 5 minutos 1 minuto
Serviço de alta capacidade 50-100 25 5 minutos 30 segundos
Tarefa em segundo plano / Ferramenta CLI 5 2 0 0
Base de Dados SQL do Azure (Basic/Standard) 10-20 5 5 minutos 1 minuto

Tip

Defina MaxOpenConns abaixo do limite de ligação da sua instância do SQL Server ou do nível SQL do Azure. Exceder o máximo de ligações concorrentes do servidor causa falhas de login para todos os clientes.

Valores curtos de ConnMaxLifetime e ConnMaxIdleTime reduzem a probabilidade de ligações inativas após failover ou reciclagem do gateway, mas também aumentam a rotatividade das ligações. Se a sua aplicação estabelecer ligação diretamente a uma instância estável do SQL Server e não estiver a detetar falhas causadas por ligações desatualizadas, é razoável deixar ambos os valores em 0.

Monitorizar estatísticas do pool

Use db.Stats() para ler estatísticas atuais do grupo:

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-chave:

Campo Description
OpenConnections Total de ligações abertas (em uso + inativa).
InUse Ligações atualmente verificadas pelos chamadores.
Idle Conexões em espera no pool.
WaitCount Número total de vezes que um chamador teve de esperar por uma ligação.
WaitDuration Tempo total acumulado de espera.

Se WaitCount está a crescer de forma constante, não aumente MaxOpenConns automaticamente. Primeiro, verifique se as linhas, transações e ligações dedicadas estão a ser encerradas rapidamente, e confirme que o servidor consegue suportar um pool maior.

SessionInitSQL

Use-se SessionInitSQL para executar uma instrução SQL em cada nova ligação à medida que entra no pool. Esta funcionalidade é útil para definir opções ao nível da sessão:

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)

Fixação de ligação

Certas operações fixam uma ligação para que não seja devolvida ao pool até o fim da operação:

  • Transações (db.BeginTx) - A ligação permanece associada até que Commit() ou Rollback() seja chamado.
  • Ligações únicas (db.Conn) - A ligação fica fixa até conn.Close() ser invocada.
  • Linhas abertas (db.QueryContext) - A ligação é fixada até rows.Close() ser chamada.

Feche sempre estes recursos prontamente para evitar esgotar o pool.

Detetar e resolver o esgotamento da piscina

O esgotamento da piscina ocorre quando todas as ligações estão em uso e a piscina atingiu MaxOpenConns. Os novos chamadores bloqueiam até que a ligação seja retornada. Os sintomas incluem alta latência, acumulação de gorotinas e eventuales erros de prazo de contexto.

Monitorizar sinais de exaustão

Inquérito db.Stats() periodicamente e alerte quando for detetada contenção:

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 comuns e soluções

Motivo Symptom Solução
MaxOpenConns demasiado baixo para a carga de trabalho WaitCount cresce de forma constante. Aumente MaxOpenConns.
Linhas não fechadas em caminhos de erro InUse cresce, Idle mantém-se em 0. Use defer rows.Close() imediatamente após QueryContext.
Transações de longa duração InUse mantém-se alto. Mantenha as transações curtas. Utilize tempos limite de contexto.
db.Conn usado desnecessariamente InUse mais alto do que o esperado. Só usa db.Conn quando precisares de estado com âmbito de sessão (tabelas temporárias).
MaxOpenConns não definido (ilimitado) Centenas de ligações abertas sob carga. Defina sempre MaxOpenConns para um valor limitado.

Verificações de saúde e deteção de ligações obsoletas

O pool não valida ativamente as ligações inativas. Uma ligação que ficou inativa enquanto o servidor a reciclava falha na próxima utilização. Configure ConnMaxLifetime e ConnMaxIdleTime para alternar as ligações antes de ficarem inativas:

// 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 fecha ligações ociosas de forma proativa, os registos do SQL Server podem mostrar ligações a serem cortadas. Este comportamento é esperado, não uma fuga de conexão. Se o seu DBA indicar encerramentos inesperados de ligações, verifique se a definição ConnMaxIdleTime está alinhada com as expectativas de monitorização da equipa.

Se a sua aplicação se ligar através de um balanceador de carga ou do SQL do Azure com geo-replicação, defina ConnMaxLifetime para 5 minutos, ou menos. Esta configuração garante que as ligações são redistribuídas entre réplicas após um failover.

Validar a conectividade no arranque

Ligue db.PingContext sempre após abrir a base de dados para confirmar que a cadeia de ligação está correta e que o servidor está acessível:

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 do conjunto

Exponha as estatísticas do pool ao seu sistema de monitorização, lendo periodicamente db.Stats():

Exemplo de Prometheus

Registe medidores que acompanham as estatísticas do pool e atualize-os 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
        }
    }
}

Lista de verificação da configuração do pool

Area Recommendation
MaxOpenConns Sempre definido para um valor limitado. Ajuste-a ao nível de concorrência da sua carga de trabalho, mantendo-se abaixo do limite de ligações do servidor.
MaxIdleConns Defina como pelo menos metade de MaxOpenConns. Poucas ligações em repouso causam sobrecarga frequente de religação.
ConnMaxLifetime Defina para 5 minutos para SQL do Azure ou ambientes com balanceamento de carga. Evita a acumulação de ligações obsoletas.
ConnMaxIdleTime Defina para 30-60 segundos para fechar ligações que já não são necessárias.
Monitoring Monitorize db.Stats() e configure alertas para o crescimento de WaitCount.
Limpeza de recursos Sempre defer rows.Close(), defer tx.Rollback(), e defer conn.Close().
Validação de arranque Ligue db.PingContext depois sql.Open para verificar a conectividade.