홈 | Tech Notes

홈 | Tech Notes

대용량 데이터 수집 파이프라인에서 CPython 순환 참조와 가비지 컬렉션 지연으로 인한 OOM 현상을 해결하고 메모리 최적화를 이뤄낸 실무 노하우를 정리합니다.

1. 로컬에서는 멀쩡하던 배치가 왜 죽었을까

대용량 데이터를 수집하는 파이프라인을 운영에 올리고 얼마 지나지 않아 컨테이너가 OOM(Out of Memory)으로 죽는 일이 벌어졌습니다.

처음에는 원인을 짐작하기 어려웠습니다. 로컬에서 수십 건의 샘플로 테스트할 때는 메모리 사용량이 아주 안정적이었기 때문입니다. 하지만 수천 건의 문서를 쉬지 않고 처리하자 메모리가 계단처럼 차오르더니 결국 한계를 넘겼습니다.

2. 범인은 끝나지 않은 연결고리였다

메모리를 잡아먹고 있던 주범은 HTML 파싱에 쓰던 BeautifulSoup 객체와 수백 MB짜리 응답 JSON이었습니다.

파이썬은 어떤 변수를 가리키는 곳이 하나도 없을 때(참조 횟수가 0이 될 때) 메모리를 즉시 비웁니다. 문제는 HTML 파서 트리의 구조였습니다. 부모 태그가 자식 태그를 가리키고, 자식 태그가 다시 부모 태그를 가리키며 서로의 꼬리를 물고 있었던 것입니다.

이렇게 객체끼리 서로를 붙잡고 있으면 함수가 끝나거나 단순히 del을 호출해도 파이썬은 "아직 누군가 쓰고 있다"고 판단해 메모리를 바로 돌려주지 않습니다.

결국 다 쓴 트리는 연결고리부터 직접 끊어내야 했습니다.

# 서로 물고 있는 노드 연결고리를 먼저 끊은 뒤 메모리 해제
soup.decompose()
del soup
gc.collect()

3. 한 줄을 더한 뒤 달라진 그래프

soup.decompose()로 트리 내부의 참조 고리를 강제로 해체하고 배치 묶음이 끝날 때마다 gc.collect()를 호출하자 효과가 바로 나타났습니다.

끝없이 우상향하던 피크 메모리가 기존 대비 60% 이상 뚝 떨어졌고, 이후 장시간 배치에서도 OOM으로 컨테이너가 재시작되는 일은 한 번도 발생하지 않았습니다.