FastAPI 비동기 처리 중 블로킹 작업 분리하기
문제 상황
사내 이미지 업로드 API에서 간헐적으로 타임아웃이 발생했다. 로그를 확인해보니 PIL을 사용한 리사이징 작업이 수행되는 동안 다른 요청들이 대기하고 있었다.
@app.post("/upload")
async def upload_image(file: UploadFile):
image = Image.open(file.file)
resized = image.resize((800, 600)) # 블로킹 발생
# ...
FastAPI는 비동기 프레임워크지만, async 함수 안에서 동기 작업을 실행하면 이벤트 루프가 블로킹된다.
해결 방법
CPU 집약적 작업은 별도 스레드에서 실행하도록 수정했다.
import asyncio
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
def resize_image_sync(image_bytes: bytes):
image = Image.open(io.BytesIO(image_bytes))
resized = image.resize((800, 600))
output = io.BytesIO()
resized.save(output, format='JPEG')
return output.getvalue()
@app.post("/upload")
async def upload_image(file: UploadFile):
image_bytes = await file.read()
loop = asyncio.get_event_loop()
resized_bytes = await loop.run_in_executor(
executor, resize_image_sync, image_bytes
)
# S3 업로드 등 후속 처리
Python 3.9 이상이라면 asyncio.to_thread()를 사용할 수도 있다.
결과
평균 응답 시간이 1.2초에서 0.4초로 감소했다. 이미지 처리 중에도 다른 요청들이 정상적으로 처리되는 것을 확인했다.
교훈
- async 함수라고 해서 모든 작업이 논블로킹인 것은 아니다
- CPU 집약적 작업은 명시적으로 스레드/프로세스 분리가 필요하다
- 운영 환경에서는 worker 수와 executor 설정의 밸런스가 중요하다