FastAPI에서 비동기 DB 커넥션 풀 관리하기

문제 상황

사내 API 서버를 FastAPI로 리팩토링하면서 asyncpg를 도입했다. 로컬에서는 문제없었는데, 스테이징 배포 후 동시 요청이 50개 정도 넘어가면 too many connections 에러가 발생했다.

원인 파악

문제는 커넥션 풀 설정이었다. 기본 설정으로는 max_size=10이었고, 각 워커 프로세스마다 별도 풀을 생성하고 있었다. Gunicorn 워커 4개 × 풀 사이즈 10 = 40개의 커넥션만 가능한 상태였다.

# 문제가 있던 코드
async def get_pool():
    return await asyncpg.create_pool(
        dsn=DATABASE_URL,
        min_size=5,
        max_size=10
    )

해결 방법

  1. 커넥션 풀을 애플리케이션 라이프사이클과 연결했다.
  2. 풀 사이즈를 워커 수와 트래픽을 고려해 조정했다.
  3. 커넥션 타임아웃을 명시적으로 설정했다.
from contextlib import asynccontextmanager
from fastapi import FastAPI

pool = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global pool
    pool = await asyncpg.create_pool(
        dsn=DATABASE_URL,
        min_size=10,
        max_size=50,
        command_timeout=60,
        max_inactive_connection_lifetime=300
    )
    yield
    await pool.close()

app = FastAPI(lifespan=lifespan)

async def get_connection():
    async with pool.acquire() as conn:
        yield conn

결과

동시 요청 200개까지 안정적으로 처리되는 것을 확인했다. 모니터링 결과 평균 커넥션 사용량은 30개 정도로 유지되고 있다.

추가로 max_inactive_connection_lifetime을 설정해서 유휴 커넥션이 자동으로 정리되도록 했다. DB 서버 부하도 줄어드는 효과가 있었다.