Python 비동기 작업 큐에서 메모리 누수 추적하기

문제 상황

이미지 리사이징을 처리하는 Celery 워커가 12시간 정도 돌면 메모리 사용량이 4GB를 넘어서 OOM Killer에 의해 종료되는 현상이 반복됐다. 작업 자체는 정상적으로 처리되고 있었기 때문에 명확한 원인을 찾기 어려웠다.

진단 과정

먼저 tracemalloc으로 메모리 스냅샷을 비교했다.

import tracemalloc

tracemalloc.start()

# 작업 실행
process_images(batch)

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

for stat in top_stats[:10]:
    print(stat)

예상외로 Pillow 라이브러리의 Image.open() 호출 부분에서 메모리가 해제되지 않고 있었다.

원인

문제는 이미지 객체를 명시적으로 닫지 않은 것이었다. Pillow의 Image 객체는 파일 디스크립터를 유지하고 있어서 GC가 즉시 회수하지 않았다.

# 문제 코드
def resize_image(path):
    img = Image.open(path)
    resized = img.resize((800, 600))
    return resized

해결

Context manager를 사용해 명시적으로 리소스를 해제하도록 수정했다.

def resize_image(path):
    with Image.open(path) as img:
        resized = img.resize((800, 600))
        # 새 이미지 객체는 원본과 독립적
        result = resized.copy()
    return result

추가로 Celery worker 설정에서 max_tasks_per_child를 1000으로 제한해 일정 작업 후 프로세스를 재시작하도록 했다.

# celery_config.py
worker_max_tasks_per_child = 1000

결과

수정 후 48시간 이상 안정적으로 동작했고, 메모리 사용량도 500MB 내외로 안정화됐다. Python의 GC가 만능은 아니며, 특히 C 확장 라이브러리를 사용할 때는 리소스 관리를 명시적으로 해야 한다는 것을 다시 확인했다.