go-mssqldbによる接続プーリング

go-mssqldbドライバーはGoのdatabase/sqlパッケージが提供する内蔵接続プールを利用しています。 すべての sql.DB インスタンスは、自動的に再利用されるアイドル接続のプールを維持しています。 この記事では、あなたのワークロードに合わせたプールの設定方法を説明します。

プールの仕組み

db.QueryContextdb.ExecContext、またはその他のデータベースメソッドを呼び出すとき:

  1. プールは使用されていない接続を見つけようとします。
  2. アイドル接続が利用できず、プールの最大サイズに達していない場合は、新しい接続が作成されます。
  3. プールが最大容量に達している場合、接続が利用可能になるまで通話がブロックされます。
  4. 操作完了後、接続はプールに戻されます。

プール構成方法

*sql.DBのメソッドを使ってプールを設定できます:

Method Description
db.SetMaxOpenConns(n) 最大開通接続数(使用中+アイドル)。 デフォルト: 0 (無制限)。
db.SetMaxIdleConns(n) プール内の最大アイドル接続数。 既定値: 2
db.SetConnMaxLifetime(d) 接続が再利用できる最大総時間。 デフォルト: 0 (制限なし)。
db.SetConnMaxIdleTime(d) 接続が閉じるまでにアイドル状態にできる最大時間。 デフォルト: 0 (制限なし)。

Example

データベースを開いた直後にプールを設定してください:

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)

これらの値は普遍的なデフォルトではなく、出発点として扱ってください。 多くのサービスでは、まず MaxOpenConnsMaxIdleConns に上限と下限を設定し、デプロイ構成によって古い接続が残ったり、接続の分散が偏ったりする可能性がある場合にのみ、接続の有効期間とアイドル時間の制限を追加します。

シナリオ マックスオープン MaxIdle マックスライフタイム MaxIdleTime
安定したSQL Serverネットワークパス上のWebアプリケーション 25 10 0 0
Azure SQL、ゲートウェイ、またはロードバランサーを通じたウェブアプリケーション 25 10 5 分 1 分
高スループットサービス 50-100 25 5 分 30 秒
バックグラウンドジョブ/CLIツール 5 2 0 0
Azure SQL Database (Basic/Standard) 10-20 5 5 分 1 分

Tip

MaxOpenConnsSQL ServerインスタンスまたはAzure SQL階層の接続制限より低く設定してください。 サーバーの最大同時接続数を超えると、すべてのクライアントでログイン失敗を引き起こします。

ConnMaxLifetimeConnMaxIdleTime の値を短くすると、フェイルオーバーやゲートウェイのリサイクル後に古い接続が残る可能性を減らせますが、その一方で接続の頻繁な作成と切断が増えます。 アプリが安定した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)
            }
        }
    }
}

一般的な原因と解決策

原因 症状: ソリューション
MaxOpenConns 仕事量に対して低すぎる WaitCount 着実に成長しています。 MaxOpenConnsを増やします。
エラーパスで閉じていない行 InUse 成長し、 Idle 0のままです。 defer rows.Close()の直後にすぐに使QueryContext
長時間トランザクション InUseは高いままです。 取引は短くしましょう。 コンテキストタイムアウトを使いましょう。
db.Conn が不要に使用されている InUse 予想以上に高くなっていた。 セッションスコープ状態(一時テーブル)が必要な時だけ db.Conn 使ってください。
MaxOpenConns セットなし(無制限) 高負荷時に数百のオープン接続があります。 MaxOpenConnsを必ず有界値に設定してください。

ヘルスチェックと古い接続検出

プールはアイドル状態の接続を能動的には検証しません。 サーバーが再利用している間にアイドル状態だった接続は、次の使用時に失敗します。 接続が古くなる前に ConnMaxLifetimeConnMaxIdleTime を回転させるように設定してください:

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

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 レコメンデーション
MaxOpenConns 常に有界値に設定してください。 サーバーの接続制限より下のワークロードの並行処理に合わせて調整してください。
MaxIdleConns 少なくとも MaxOpenConnsの半分に設定してください。 アイドル接続が少なすぎると、頻繁な再接続のオーバーヘッドが生じます。
ConnMaxLifetime Azure SQLや負荷分散環境の場合は5分に設定してください。 古い接続の蓄積を防ぎます。
ConnMaxIdleTime 不要な接続を閉じるまで30〜60秒に設定してください。
Monitoring db.Stats() の増加をポーリングし、WaitCount に対してアラートを出します。
リソースのクリーンアップ いつも defer rows.Close()defer tx.Rollback()defer conn.Close()
スタートアップ検証 db.PingContext後にsql.Openに連絡して接続を確認してください。