FastAPI 비동기 처리 중 DB 커넥션 풀 고갈 이슈
문제 상황
Flask 기반 API 서버를 FastAPI로 마이그레이션한 후, 동시 요청이 50개를 넘어가면 TimeoutError: QueuePool limit exceeded가 발생했다. 기존 Flask에서는 문제가 없었던 부분이라 당황스러웠다.
원인 분석
FastAPI는 기본적으로 비동기 처리를 하지만, SQLAlchemy 1.4의 동기 방식 엔진을 그대로 사용하고 있었다. 각 요청마다 커넥션을 획득하지만 await가 아닌 블로킹 방식으로 동작해서, 실제로는 커넥션을 오래 점유하는 상황이었다.
# 문제가 있던 코드
engine = create_engine(
DATABASE_URL,
pool_size=10,
max_overflow=20
)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
with SessionLocal() as session:
user = session.query(User).filter(User.id == user_id).first()
return user
해결 방법
SQLAlchemy 1.4의 async 엔진으로 전환하고, 세션 관리를 의존성 주입 패턴으로 변경했다.
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
engine = create_async_engine(
DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://"),
pool_size=20,
max_overflow=40,
pool_pre_ping=True
)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async def get_db():
async with AsyncSessionLocal() as session:
yield session
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
return user
결과
동시 요청 200개까지 안정적으로 처리되는 것을 확인했다. 응답 시간도 평균 120ms에서 80ms로 개선됐다. asyncpg 드라이버의 성능이 psycopg2보다 확실히 좋다는 것도 체감할 수 있었다.
비동기 프레임워크를 쓴다면 DB 레이어까지 완전히 비동기로 가는 것이 맞다는 교훈을 얻었다.