mssql-python을 이용한 연결 풀링

연결 풀링은 각 요청마다 새로운 연결을 생성하는 대신 데이터베이스 연결을 재사용하여 애플리케이션 성능을 향상시킵니다. 연결을 열려면 여러 번의 시간이 많이 소요되는 단계를 거쳐야 합니다:

  • 드라이버는 네트워크 소켓을 설정합니다.
  • 운전자가 TLS 핸드셰이크를 완료합니다.
  • 드라이버가 서버와 인증합니다.
  • 드라이버는 연결 매개변수를 검증합니다.

연결 풀링은 연결을 계속 열어두고 재사용할 수 있게 해주므로, 앱이 각 요청마다 이 과정을 반복할 필요가 없습니다.

기본 동작

첫 연결을 만들 때 기본적으로 연결 풀링이 활성화 되어 있습니다. 기본 설정은 다음과 같습니다.

설정 기본값 설명
max_size 100 각 고유한 연결 문자열당 최대 연결 수.
idle_timeout 600초(10분) 유휴 연결이 닫히기 전까지의 시간(초)
import mssql_python

# Pooling is automatically enabled with defaults
conn = mssql_python.connect(connection_string)

연결 풀링을 구성합니다

연결을 생성하기 전에 풀링을 구성하세요:

import mssql_python

# Configure custom pool settings
mssql_python.pooling(max_size=50, idle_timeout=300)

# Now create connections
conn = mssql_python.connect(connection_string)

Parameters

함수는 pooling() 다음 매개 변수를 허용합니다.

매개 변수 Type Default 설명
max_size int 100 연결 문자열당 최대 풀링 연결 수.
idle_timeout int 600 유휴 연결이 풀에서 제거되기 전까지의 시간(초).
enabled bool True 풀링을 활성화하거나 비활성화하세요.

연결 풀링 비활성화

풀링을 비활성화하려면 연결을 생성하기 전에 pooling()와 함께 enabled=False를 호출하세요:

import mssql_python

mssql_python.pooling(enabled=False)

# Connections are now created and destroyed per use
conn = mssql_python.connect(connection_string)

비고

연결을 설정하기 전에 풀링 설정을 설정하세요. 연결을 만든 후에 전화해도 pooling() 아무런 효과가 없습니다.

풀링 작동 원리

연결 끈 절연

각 고유 연결 문자열은 각각 독립적인 풀을 유지합니다. 풀은 서로 다른 연결 문자열 간에 연결을 공유하지 않습니다:

# These use separate pools
conn1 = mssql_python.connect("Server=<server1>;Database=<database1>;...")
conn2 = mssql_python.connect("Server=<server2>;Database=<database2>;...")

정체성 고립

대부분의 토큰 기반 인증 방식에서는 드라이버가 연결을 열어주는 Microsoft Entra 신원에 따라 풀을 분리하므로, 한 주체로 인증된 연결이 다른 주체를 사용하는 호출자에게 전달되지 않습니다. 예외는 다음 표의 첫 번째 행인데, 여기서 키는 연결 문자열만입니다.

풀 키는 인증 방법에 따라 다릅니다:

Authentication 풀 키
SQL 인증, 신뢰할 수 있는 연결, 서비스 주체, Windows 통합 인증, 그리고 ActiveDirectoryInteractive Windows 위에서 연결 문자열만
관리형 아이덴티티 연결 문자열과 클라이언트 ID 또는 시스템 할당 식별
장치 코드 인증과 Windows 이외 플랫폼에서의 인터랙티브 인증 연결 문자열과 로그인된 계정
DefaultAzureCredential, 사용자 지정 token_provider, 그리고 가공되지 않은 액세스 토큰 연결 문자열과 토큰의 해시 값

토큰 해시가 마지막 그룹의 키의 일부이기 때문에, 각 독립된 토큰은 고유한 풀을 받습니다. 캐시된 토큰을 반환하는 수명이 긴 자격 증명은 단일 풀을 지속적으로 재사용합니다. 드라이버는 백그라운드 스레드로 처리하는 대신, 이후 수행되는 풀 작업 중에 유휴 ID 풀을 필요에 따라 회수합니다.

운전자는 새로운 연결을 열어야 할 때만 토큰을 획득하므로, 풀 히트는 토큰을 획득하지 않습니다.

드라이버는 토큰 만료까지 5분 이하로 남은 풀링된 연결을 연결이 클라이언트에 반환되기 전에 갱신하므로, 수명이 긴 풀이 곧 실패할 연결을 반환하지 않도록 합니다.

연결 수명 주기

획득(연결 확보):

  1. 풀은 유휴 시간 만료(idle-expired)된 오래된 연결을 제거합니다.
  2. 풀은 기존 연결을 재사용하려고 시도합니다:
    • 연결이 살아있는지 확인합니다.
    • 연결 상태를 초기화합니다.
    • 두 검사가 모두 성공하면 연결을 반환합니다.
  3. 재사용 가능한 연결이 없고 풀 크기가 max_size 미만이면 드라이버가 새 연결을 생성합니다.
  4. 풀이 최대 용량에 도달한 상태에서 유효한 연결이 없으면 드라이버가 오류를 발생시킵니다.

해제(연결 반환):

  1. 수영장에 용량이 있으면 연결을 저장해 재사용합니다.
  2. 풀이 max_size 상태이면 드라이버가 즉시 연결을 종료합니다.

연결 상태 확인

드라이버는 풀된 연결을 재사용하기 전에 연결 상태 점검을 수행합니다.

  1. 살아 있음 체크: 네트워크 연결이 여전히 유효한지 확인합니다.
  2. 리셋 점검: 세션 상태(격리 수준, 설정)를 초기화하여 깨끗한 재사용을 합니다.

어느 쪽이든 실패하면 풀은 연결을 버리고 새로운 연결을 생성합니다.

자동 정리

  • 유휴 시간 초과: 드라이버는 idle_timeout 값보다 오래 사용되지 않은 연결을 닫습니다.
  • 프로세스 종료: 핸들러는 atexit Python 프로세스가 종료될 때 모든 풀링 연결을 종료합니다.

모범 사례

수영장 크기를 적절히 정하세요

풀 크기를 애플리케이션의 동시성에 맞추세요.

# For a web application with 20 concurrent requests
mssql_python.pooling(max_size=25)  # Slightly more than expected concurrency

컨텍스트 매니저 사용

컨텍스트 관리자를 사용하면 연결을 풀에 올바르게 반환할 수 있습니다.

with mssql_python.connect(connection_string) as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT TOP 5 Name, ListPrice FROM Production.Product")
    rows = cursor.fetchall()
# Connection returned to pool

연결 줄을 일정하게 유지하세요

연결 문자열 내 서로 다른 매개변수는 별도의 풀을 만듭니다.

# These create THREE separate pools (inefficient)
conn1 = mssql_python.connect("Server=<server>;Database=<database>;Encrypt=yes;")
conn2 = mssql_python.connect("SERVER=<server>;DATABASE=<database>;ENCRYPT=yes;")  # Different case
conn3 = mssql_python.connect("Server=<server>;Database=<database>;Encrypt=yes;", timeout=30)  # Extra parameter

# Use a constant connection string instead
CONNECTION_STRING = "Server=<server>;Database=<database>;Encrypt=yes;"
conn1 = mssql_python.connect(CONNECTION_STRING)
conn2 = mssql_python.connect(CONNECTION_STRING)  # Same pool

Azure SQL 연결 제한 고려

Azure SQL Database는 서비스 계층에 따라 연결 제한을 강제합니다. 다음 값들은 대략적인 수치입니다; 전류 제한에 관한 링크된 문서를 확인하세요:

서비스 계층 최대 동시 연결
Basic 30
표준 S0-S2 60-120
표준 S3 및 이후 버전 200
Premium 500

이 한도 이하로 가치를 조정하세요 max_size .

# For Azure SQL Standard S2 (120 limit)
mssql_python.pooling(max_size=100)  # Leave headroom

작업 부하에 맞게 유휴 타임아웃을 조정하세요

  • 빈번한 연결: 연결을 활성 상태로 유지하려면 더 긴 idle_timeout 값을 사용하세요.
  • 산발적인 연결: 자원을 방출할 때 더 idle_timeout 짧은 값을 사용하세요.
# High-frequency API: keep connections warm
mssql_python.pooling(idle_timeout=1800)  # 30 minutes

# Batch job running every hour: release between runs
mssql_python.pooling(idle_timeout=60)  # 1 minute

Limitations

현재 구현은 다른 드라이버에 비해 몇 가지 한계가 있습니다:

특징 상태
ClearPool() / ClearAllPools() 사용할 수 없음.
풀 통계 및 모니터링 사용할 수 없음.
연결별 풀 오버라이드 사용할 수 없음.
최소 풀 크기 구성할 수 없습니다.

예시: 웹 애플리케이션 패턴

다음 Flask 예시는 요청 간에 연결이 투명하게 풀링되는 방식을 보여줍니다:

import mssql_python
from flask import Flask, g

app = Flask(__name__)

# Configure pooling at startup
mssql_python.pooling(max_size=20, idle_timeout=300)

def get_db():
    if 'db' not in g:
        g.db = mssql_python.connect(app.config['DATABASE_URL'])
    return g.db

@app.teardown_appcontext
def close_db(error):
    db = g.pop('db', None)
    if db is not None:
        db.close()  # Returns to pool

@app.route('/products')
def list_products():
    conn = get_db()
    cursor = conn.cursor()
    cursor.execute("SELECT TOP 5 Name, ListPrice FROM Production.Product")
    return cursor.fetchall()

수영장 고갈을 인식하세요

풀 내 모든 연결이 사용 중일 때 새 연결을 요청하면 다음과 같은 증상이 나타납니다:

  • 사용 가능한 연결을 기다리는 동안 연결이 멈추거나 시간 초과됩니다.
  • 애플리케이션 처리량이 부하 시 갑자기 감소합니다.
  • 드라이버가 재사용할 수 없는 연결을 생성하면서 메모리 사용량이 증가합니다.

일반적인 원인:

  • 연결은 풀로 반환되지 않습니다. 끝나면 항상 연결을 닫거나, 컨텍스트 매니저를 사용하세요. 닫히지 않은 연결은 체크아웃된 상태로 유지됩니다.
  • 풀 크기가 업무량에 비해 너무 작아요. 만약 50개의 동시 요청 max_size=20이 있지만 30개의 요청이 대기 중이라면,
  • 장기 실행 쿼리는 연결을 유지합니다. 긴 작업을 나누거나 배치 작업을 위해 전용 연결을 사용하세요.

해결 방법:

# 1. Always use context managers to guarantee return
with mssql_python.connect(connection_string) as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT ...")
    rows = cursor.fetchall()
# Connection returned to pool here, even if an exception occurs

# 2. Size the pool to match your concurrency
mssql_python.pooling(max_size=50)  # Match or slightly exceed expected concurrent connections

# 3. Reduce idle timeout if connections go stale
mssql_python.pooling(idle_timeout=120)