Пул соединений с go-mssqldb

Драйвер go-mssqldb использует встроенный пул соединений, предоставляемый пакетом database/sql языка Go. Каждый экземпляр sql.DB поддерживает пул неактивных соединений, которые автоматически переиспользуются. В этой статье объясняется, как настроить пул под вашу нагрузку.

Как работает бассейн

Когда вы вызываете db.QueryContext, db.ExecContext, или любой другой метод базы данных:

  1. Пул пытается найти свободное соединение.
  2. Если свободных соединений нет и пул ещё не достиг максимального размера, создаётся новое соединение.
  3. Если пул на максимальной мощности, вызов блокируется до тех пор, пока не станет доступно соединение.
  4. После завершения операции соединение возвращается в пул.

Методы конфигурации пула

Настройте пул с помощью методов объекта *sql.DB:

Метод Description
db.SetMaxOpenConns(n) Максимальное количество открытых соединений (используемых + неактивных). По умолчанию: 0 (без ограничений).
db.SetMaxIdleConns(n) Максимальное количество неактивных соединений в пуле. По умолчанию: 2.
db.SetConnMaxLifetime(d) Максимальное общее время повторного использования соединения. По умолчанию: 0 (без ограничения).
db.SetConnMaxIdleTime(d) Максимальное время, в течение которого соединение может простоять перед закрытием. По умолчанию: 0 (без ограничения).

Пример

Настройте пул сразу после открытия базы данных:

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)

Воспринимайте эти значения как отправную точку, а не как универсальные стандарты. Для многих сервисов первым полезным шагом будет задать ограниченные значения для MaxOpenConns и MaxIdleConns, а ограничения по времени жизни и простою добавлять только тогда, когда сценарий развертывания может приводить к устаревшим или неравномерно распределённым соединениям.

Сценарий MaxOpen MaxIdle MaxLifetime MaxIdleTime
Веб-приложение на стабильном сетевом пути SQL Server 25 10 0 0
Веб-приложение через Azure SQL, шлюз или балансировщик нагрузки 25 10 5 мин 1 минута
Сервис с высокой пропускной способностью 50-100 25 5 мин 30 секунд
Фоновая работа / инструмент CLI 5 2 0 0
База данных SQL Azure (Basic/Standard) 10-20 5 5 мин 1 минута

Tip

Установите значение MaxOpenConns ниже предела количества подключений для вашего экземпляра SQL Server или уровня службы Azure SQL. Превышение максимального количества одновременных соединений сервера приводит к сбоям входа для всех клиентов.

Короткие ConnMaxLifetime и ConnMaxIdleTime значения снижают вероятность застоявшихся соединений после отказа или повторного использования шлюза, но также увеличивают отток соединений. Если ваше приложение напрямую подключается к стабильному экземпляру SQL Server и вы не видите сбоев застоявшегося соединения, оставлять оба значения на уровне 0 — разумно.

Отслеживание статистики пула

Используйте db.Stats() для чтения текущей статистики пула:

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)

Ключевые поля:

Поле Description
OpenConnections Общее количество открытых соединений (в использовании + в простое).
InUse Соединения сейчас проверены звонящими.
Idle Соединения ждут в бассейне.
WaitCount Общее количество раз, когда звонящий должен был ждать соединения.
WaitDuration Общее накопленное время ожидания.

Если WaitCount растёт стабильно, не увеличивайте MaxOpenConns автоматически. Сначала убедитесь, что строки, транзакции и выделенные соединения быстро закрываются, и убедитесь, что сервер поддерживает более крупный пул.

SessionInitSQL

Используйте SessionInitSQL, чтобы выполнять SQL-запрос для каждого нового соединения при его поступлении в пул соединений. Эта функция полезна для настройки параметров на уровне сессии:

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)

Закрепление соединения

Некоторые операции закрепляют соединение, чтобы оно не возвращалось в пул до завершения операции:

  • Транзакции (db.BeginTx) — соединение закрепляется до Commit() вызова или Rollback() вызова.
  • Одиночные соединения (db.Conn) — соединение остаётся закреплённым до вызова conn.Close().
  • Открытые строки (db.QueryContext) — соединение закрепляется до rows.Close() вызова.

Всегда закрывайте эти ресурсы как можно скорее, чтобы не истощить пул.

Обнаружить и устранить истощение бассейна

Исчерпание пула происходит, когда все соединения используются и пул достиг предела MaxOpenConns. Новые звонящие блокируют до восстановления соединения. Симптомы включают высокую задержку, накопление горутинов и возможные ошибки в контекстном дедлайне.

Монитор усталости

Периодически проводите опросы db.Stats() и оповещайте при обнаружении спора:

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

Распространенные проблемы и их решения

Причина Симптом Solution
MaxOpenConns слишком низкая для нагрузки WaitCount устойчиво растёт. Увеличьте MaxOpenConns.
Строки, не замкнутые в путях ошибок InUse Растёт, Idle остаётся на 0. Используйте defer rows.Close() сразу после QueryContext.
Длительные транзакции InUse Держится высоко. Держите транзакции короткими. Используйте контекстные тайм-ауты.
db.Conn использовался без нужды InUse выше, чем ожидалось. Используйте db.Conn только тогда, когда вам нужно состояние в рамках сеанса (временные таблицы).
MaxOpenConns не задано (без ограничений) Сотни открытых соединений под нагрузкой. Всегда задавайте для MaxOpenConns ограниченное значение.

Проверки здоровья и обнаружение устаревших соединений

Пул не выполняет активную проверку неактивных соединений. Соединение, которое простояло, пока сервер его перерабатывал, выходит из строя при следующем использовании. Настройте ConnMaxLifetime и ConnMaxIdleTime так, чтобы они выполняли ротацию соединений до того, как те устареют:

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

Замечание

Когда ConnMaxIdleTime заблаговременно закрывает неактивные соединения, в журналах SQL Server может отображаться разрыв соединений. Это ожидаемое поведение, а не утечка соединения. Если ваш DBA сообщает о неожиданных закрытиях соединения, убедитесь, что ConnMaxIdleTime настройка соответствует ожиданиям команды по мониторингу.

Если ваше приложение подключается через балансировщик нагрузки или Azure SQL с георепликацией, установите ConnMaxLifetime на 5 минут или меньше. Эта настройка обеспечивает перераспределение соединений между репликами после отказа.

Проверка подключения при запуске

Всегда вызывайте db.PingContext после открытия базы данных, чтобы убедиться, что строка подключения указана правильно и сервер доступен:

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

Метрики экспортного пула

Предоставьте статистику пула своей системе мониторинга, периодически считывая db.Stats():

Пример Прометея

Зарегистрируйте метрики, которые отслеживают статистику пула и периодически обновляются:

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

Контрольный список конфигурации пула

Area Recommendation
MaxOpenConns Всегда устанавливайте ограниченное значение. Сопоставьте это с уровнем параллелизма вашей рабочей нагрузки, не превышая лимит подключений сервера.
MaxIdleConns Установите не менее половины от MaxOpenConns. Слишком малое количество неактивных соединений приводит к частым издержкам на повторное подключение.
ConnMaxLifetime Установите значение 5 минут для Azure SQL или сред с балансировкой нагрузки. Это предотвращает накопление застоявшихся соединений.
ConnMaxIdleTime Установите на 30-60 секунд, чтобы закрыть соединения, которые больше не нужны.
Контроль Отслеживайте db.Stats() и настраивайте оповещения о росте WaitCount.
Очистка ресурсов Всегда defer rows.Close(), defer tx.Rollback(), и defer conn.Close().
Валидация запуска Позвони db.PingContext после sql.Open , чтобы подтвердить связь.