이 글은 이메일로 굴러가던 물류 운영을, 데이터로 바꿨다 — 메일함 AI 인제스천 파이프라인 전 과정 시리즈의 일부입니다.
문서 파싱 자동화를 작은 클라우드 VM 위에서 돌리고 있었다. 어느 순간부터 워크플로우를 실행하면 502가 뜨고 서비스가 재시작되는 일이 반복됐다. 실행이 무겁긴 했지만 죽을 정도는 아니라고 생각했는데, 계속 죽었다.
이 글은 그 502의 진짜 원인을 찾아가는 기록이다. 결론부터 말하면 CPU가 아니라 메모리였고, 엉뚱한 곳을 한참 봤다.
처음엔 CPU를 의심했다
실행 중에 서버 상태를 보니 CPU가 100%를 넘겨 찍고 있었다. “역시 연산이 무거워서 뻗는구나” 싶었다. 그런데 이건 오판이었다. 코어가 두 개인 환경에서 순간적으로 110%대가 찍히는 건 정상 범위다. CPU는 범인이 아니었다.
502의 정체는 다른 데 있었다. 502는 앞단(리버스 프록시)이 뒤의 애플리케이션에 연결하지 못할 때 뜨는 코드다. 즉 뒤에서 프로세스가 죽고 재시작하는 동안 앞단이 갈 곳을 잃은 것이다. CPU가 아니라 “프로세스가 왜 죽나”를 봐야 했다.
dmesg 한 줄이 범인을 지목했다
프로세스가 죽는 이유를 찾을 땐 커널 로그를 본다. dmesg를 열자 답이 한 줄로 있었다.
Out of memory: Killed process (node)
메모리 부족이었다. 리눅스 커널의 OOM Killer(Out Of Memory Killer)가 메모리를 가장 많이 쓰는 프로세스를 강제 종료한 것이다. 그 프로세스가 워크플로우 엔진(node)이었다.
원인이 선명해졌다. 이 VM은 물리 메모리가 2GB가 안 됐고, swap이 아예 0이었다. 그런데 문서 파싱은 PDF를 base64로 인코딩해 메모리에 통째로 올린다. 파일 몇 개가 겹치면 프로세스 메모리가 순식간에 1.5GB 근처로 튀고, 물리 메모리 상한을 넘는 순간 OOM Killer가 프로세스를 죽인다. → 재시작 → 그 사이 502. 반복의 정체가 이거였다.

두 겹으로 해결했다
응급 처치 — swap 추가. 먼저 죽는 걸 막아야 했다. swap 2GB를 만들어 붙였다.
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# 재부팅 후에도 유지되도록 /etc/fstab에 등록
swap은 메모리가 순간적으로 넘칠 때 디스크로 흘려보내 즉사를 막아준다. 다만 이건 완충일 뿐 근본은 아니다.
근본 처치 — 큰 파일을 메모리에 안 들기. 진짜 해결은 워크플로우 엔진이 바이너리 데이터(PDF)를 메모리가 아니라 디스크에 두도록 설정을 바꾸는 것이었다. 엔진에는 바이너리를 메모리에 들고 다닐지, 파일시스템에 떨궈놓고 참조만 할지 고르는 설정이 있다. 이걸 파일시스템 모드로 바꾸니 PDF가 프로세스 메모리를 밀어올리지 않게 됐다. 효과가 가장 컸다.
여기에 실행 기록이 무한히 쌓여 저장소를 채우지 않도록 오래된 실행 데이터를 일정 기간 후 자동 정리(prune)하도록 함께 걸었다.
곁다리로 만난 429 — 이건 throttle 문제였다
메모리를 잡고 나니 이번엔 메일 라벨을 수정하는 단계에서 Too many concurrent requests(429)가 떴다. 여러 건을 동시에 라벨링하면서 한꺼번에 요청을 쏟아부은 것이다.
해결의 축은 재시도가 아니라 throttle였다. 처음엔 “재시도 몇 번 걸면 되겠지” 했는데, 재시도는 이미 터진 요청을 다시 던질 뿐 동시 요청 자체를 줄이지 못한다. 근본은 노드의 배치(batch) 설정으로 한 번에 하나씩, 요청 사이에 간격을 두게 만드는 것이었다. 재시도는 그 위에 보조로 얹었다.
배운 것
- 502에서 CPU부터 보지 마라. 502는 “뒤가 죽었다”는 신호다. 프로세스가 왜 죽는지를
dmesg로 확인하는 게 먼저다. OOM은 앱 로그가 아니라 커널 로그에 찍힌다. - 작은 VM에서 문서 파싱의 병목은 연산이 아니라 메모리다. 파일을 base64로 메모리에 올리는 순간 프로세스가 부푼다. 큰 바이너리는 디스크에 두고 참조하라.
- 배치 크기를 줄이는 건 임시방편이다. 진짜 해결은 설정(env)에 있다. 배치 축소로 넘기면 볼륨이 커질 때 같은 문제가 다시 온다.
- 429는 재시도가 아니라 throttle로 잡는다. 재시도는 터진 걸 다시 던지고, throttle은 애초에 안 터지게 한다.
한계와 다음
지금 처치는 이 규모에서의 최적이다. 처리량이 크게 늘면 swap과 설정만으로는 부족해지고, 더 넉넉한 메모리의 인스턴스로 옮기는 게 정답이 된다. 그때는 배치를 억지로 줄이지 않고 편하게 돌릴 수 있는 사양으로 올릴 계획이다.
자주 묻는 질문
Q. 502인데 왜 메모리를 봐야 하나?
502는 앞단 프록시가 뒤의 앱에 연결하지 못할 때 뜬다. 앱 프로세스가 OOM으로 죽고 재시작하는 동안 연결이 끊겨 502가 나는 것이다. 그래서 “앱이 왜 죽나”가 핵심이고, OOM은 커널 로그(dmesg)에 찍힌다.
Q. swap만 넉넉히 주면 해결되나?
swap은 즉사를 막는 완충일 뿐이다. 큰 바이너리를 계속 메모리에 올리는 구조 자체를 바꾸지 않으면, swap이 심하게 쓰이며 느려지고 결국 한계에 부딪힌다. 데이터를 디스크에 두는 설정이 근본이다.
Q. 배치 크기를 1로 줄이면 되지 않나?
그러면 당장은 안 죽지만, 처리량이 늘면 같은 벽에 다시 부딪힌다. 배치 축소는 임시방편이고, 메모리 설정과 인스턴스 사양이 근본 해결이다.