go-mssqldb를 사용한 연결 풀링

드라이버는 go-mssqldb Go의 database/sql 패키지가 제공하는 내장 연결 풀을 사용합니다. 모든 sql.DB 인스턴스는 자동으로 재사용되는 유휴 연결 풀을 유지합니다. 이 글에서는 워크로드에 맞는 풀을 어떻게 구성하는지 설명합니다.

수영장 작동 원리

db.QueryContext, db.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를 설정하고, 배포 경로로 인해 오래되었거나 불균등하게 분산된 연결이 생길 수 있는 경우에만 수명 및 유휴 제한을 추가하는 것이 유용한 첫 단계입니다.

시나리오 맥스오픈 맥스아이들 맥스라이프타임 MaxIdleTime
안정적인 SQL Server 네트워크 경로 위의 웹 애플리케이션 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

SQL Server 인스턴스나 Azure SQL 계층의 연결 한도 이하로 설정 MaxOpenConns 하세요. 서버의 최대 동시 연결을 초과하면 모든 클라이언트에서 로그인 실패가 발생합니다.

짧은 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)

키 필드:

Field Description
OpenConnections 총 개방 연결 (사용 중+유휴).
InUse 현재 호출자가 사용 중인 연결.
Idle 수영장에 연결고리가 기다리고 있어.
WaitCount 전화를 걸었던 사람이 연결을 기다려야 했던 총 횟수.
WaitDuration 총 누적 대기 시간.

WaitCount가 꾸준히 증가하고 있다면 MaxOpenConns를 자동으로 늘리지 마세요. 먼저 행, 트랜잭션, 전용 연결이 신속하게 닫히고 있는지 확인하고, 서버가 더 큰 풀을 지원할 수 있는지 확인하세요.

SessionInitSQL

풀에 새 연결이 들어올 때마다 SQL 문을 실행하려면 SessionInitSQL을 사용하세요. 이 기능은 세션 레벨 옵션을 설정하는 데 유용합니다:

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으로 유지됩니다. QueryContext 직후에 defer rows.Close()를 사용하세요.
장기 실행 트랜잭션 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)

메모

ConnMaxIdleTime 유휴 연결을 사전에 닫으면 SQL Server 로그에 연결이 끊기는 모습이 나타날 수 있습니다. 이것은 예상되는 동작이지 연결 누출이 아닙니다. DBA가 예상치 못한 연결 종료를 보고하면, 설정이 ConnMaxIdleTime 팀의 모니터링 기대치와 일치하는지 확인하세요.

애플리케이션이 로드 밸런서나 Azure SQL과 지오 복제를 통해 연결된다면, 5분 이내로 설정 ConnMaxLifetime 하세요. 이 설정은 장애 조치 후 복제체 간 연결이 재분배되도록 보장합니다.

시작 시 연결 상태를 검증합니다

데이터베이스를 열면 항상 연결 문자열이 올바르고 서버에 연락 가능한지 확인하기 위해 호출 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.