Python asyncio에서 blocking 코드 처리하기
문제 상황
FastAPI로 이미지 처리 API를 만들다가 응답 속도가 현저히 느려지는 현상을 발견했다. Pillow로 이미지를 리사이징하는 작업이 동기로 동작하면서 전체 이벤트 루프를 블로킹시키고 있었다.
@app.post("/resize")
async def resize_image(file: UploadFile):
image = Image.open(file.file)
resized = image.resize((800, 600)) # 여기서 블로킹
# ...
다른 요청들도 이 작업이 끝날 때까지 대기하는 상황이었다.
해결 방법
run_in_executor를 사용해 블로킹 작업을 별도 스레드풀에서 실행하도록 변경했다.
import asyncio
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
def sync_resize(image_data):
image = Image.open(io.BytesIO(image_data))
return image.resize((800, 600))
@app.post("/resize")
async def resize_image(file: UploadFile):
image_data = await file.read()
loop = asyncio.get_event_loop()
resized = await loop.run_in_executor(
executor, sync_resize, image_data
)
# ...
결과
동시 요청 10개 기준 응답 시간이 평균 3초에서 0.5초로 개선됐다. CPU 바운드 작업은 ProcessPoolExecutor도 고려해볼만 하지만, 이미지 데이터 직렬화 오버헤드 때문에 ThreadPoolExecutor가 더 효율적이었다.
외부 sync 라이브러리를 async 환경에서 쓸 때는 항상 이벤트 루프 블로킹을 염두에 두어야 한다는 걸 다시 확인했다.