Python 멀티프로세싱으로 대용량 이미지 처리 최적화하기
문제 상황
서비스에서 사용자가 업로드한 이미지를 썸네일, 중간, 원본 3가지 사이즈로 리사이징하는 작업을 처리하고 있었다. 이미지가 하루에 수천 장씩 쌓이면서 Celery 워커가 처리하는 속도가 느려지기 시작했다.
기존에는 Pillow를 사용해 순차적으로 처리했는데, 이미지 하나당 평균 2~3초가 걸렸다.
from PIL import Image
def resize_image(input_path, output_path, size):
img = Image.open(input_path)
img.thumbnail(size, Image.ANTIALIAS)
img.save(output_path, quality=85)
# 순차 처리
for size_name, dimensions in sizes.items():
resize_image(input_path, output_path, dimensions)
멀티프로세싱 적용
Python의 GIL 때문에 멀티스레딩은 효과가 없었고, multiprocessing 모듈로 CPU 바운드 작업을 병렬화했다.
from multiprocessing import Pool
import os
def resize_task(args):
input_path, output_path, size = args
img = Image.open(input_path)
img.thumbnail(size, Image.ANTIALIAS)
img.save(output_path, quality=85)
return output_path
def process_images(input_path, sizes):
tasks = [
(input_path, f"{output_dir}/{name}.jpg", dimensions)
for name, dimensions in sizes.items()
]
with Pool(processes=os.cpu_count()) as pool:
results = pool.map(resize_task, tasks)
return results
결과
4코어 서버에서 테스트했을 때 이미지 1장당 평균 0.8초로 단축되었다. 워커 큐의 적체 현상이 해소되었고, 사용자가 업로드 후 이미지를 확인하는 시간도 크게 줄었다.
다만 프로세스 생성 오버헤드가 있어서 배치 단위로 묶어서 처리하는 것이 더 효율적이었다. 100장씩 묶어서 처리하니 전체 처리량이 추가로 20% 정도 개선되었다.
주의사항
- 프로세스 풀 크기는 CPU 코어 수에 맞춰 설정
- 메모리 사용량이 코어 수만큼 증가하므로 모니터링 필요
- Celery 워커 concurrency 설정도 함께 조정해야 함