20200224

FastAPI + SQLAlchemy 1.4 async 전환

SQLAlchemy 1.4가 async 지원 한다고 해서 프리뷰(b1 정도) 버전 물려서 FastAPI 서비스 한 개를 async db로 전환. 아직 정식은 아님, 2020년 하반기 정도에 1.4 GA 예정이라는 분위기.

현재 스택: FastAPI 0.52, SQLAlchemy 1.4.0b1, asyncpg 0.20, PostgreSQL 12.

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

engine = create_async_engine(
    "postgresql+asyncpg://user:pw@localhost/db",
    pool_size=10,
    max_overflow=5,
)

AsyncSessionLocal = sessionmaker(
    engine, class_=AsyncSession, expire_on_commit=False
)

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

쿼리 모양. 기존 session.query() 대신 select() 쓰는 2.0 스타일이 강요됨.

from sqlalchemy import select

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    stmt = select(User).where(User.id == user_id)
    result = await db.execute(stmt)
    user = result.scalar_one_or_none()
    return user

걸린 점 몇 가지 적어둠.

- expire_on_commit=False 꼭 필요. 안 하면 commit 후 속성 접근할 때 lazy load 들어가서 "another operation in progress" 예외. 처음에 한참 헤맸음.

- relationship의 lazy loading은 여전히 sync 전제. selectinload, joinedload로 eager load하거나 AsyncSession.refresh 명시 호출 필요.

- Alembic은 아직 sync 전용이라 migration은 sync 커넥션으로 돌림. 지금은 이게 맞다.

벤치는 별 의미 없는데, 동시성 200 기준 p95 latency가 sync 대비 40% 정도 좋음. 단 psycopg2 대비 asyncpg는 드라이버 자체가 빨라서 그 영향도 큼.

beta라 돌다가 내부 에러 뜨는 경우 있고, 스택트레이스가 greenlet/asyncio 섞여서 난해함. prod 넣을 때는 1.4 GA 까지 기다리는 게 현명할 듯.

댓글 없음: