이 글의 목차

“메모리를 2배로 늘렸는데 또 죽었어요”

쿠버네티스를 운영하다 보면 가장 흔하게, 그리고 가장 골치 아프게 마주치는 에러가 바로 **OOMKilled (Exit Code 137)**입니다. 개발자의 첫 반응은 보통 limits.memory를 늘리는 것입니다. 1GB에서 2GB로, 2GB에서 4GB로 말이죠. 하지만 며칠 뒤, 그 파드(Pod)는 더 비대해진 몸집으로 또다시 죽습니다.

왜 그럴까요? 단순히 메모리가 부족해서가 아니라, 메모리가 새고 있거나(Leak), 잘못된 설정 때문일 확률이 높습니다. 밑 빠진 독에 물 붓기를 멈추고, 이제 독을 수리해야 할 때입니다.

OOMKilled의 정체: 범인은 커널(Kernel)이다

파드가 OOMKilled 되었다는 건, 쿠버네티스가 임의로 죽인 것이 아닙니다. 정확히는 리눅스 커널의 OOM Killer가 “이 컨테이너가 할당된 메모리 한계(cgroup limit)를 넘었네? 시스템 전체를 살리기 위해 널 죽여야겠다.” 하고 SIGKILL (9) 시그널을 보낸 것입니다. Exit Code 137은 128 + 9 (SIGKILL)을 의미합니다. 즉, 외부 요인에 의해 강제 종료당했다는 뜻입니다.

왜 죽었을까? 3가지 유력한 용의자

  1. 진짜 메모리 누수 (Memory Leak): 코드 어딘가에서 객체를 계속 생성하고 해제하지 않는 경우입니다. 전역 변수에 데이터를 계속 쌓거나, 닫히지 않은 고루틴/스레드가 누적되는 경우가 흔합니다. 이 경우 리미트를 아무리 늘려도 결국엔 죽습니다.

  2. 순간적인 트래픽 폭주 (Spike): 평소엔 얌전하다가, 특정 API 호출 시 대용량 데이터를 메모리에 한 번에 로딩하는 경우입니다. 예를 들어, 100만 건의 엑셀 데이터를 스트리밍하지 않고 한 번에 읽어서 처리하려고 하면 순간적으로 메모리 사용량이 치솟아 죽게 됩니다.

  3. 잘못된 JVM/Runtime 설정: Java 앱의 경우, 컨테이너 메모리는 2GB인데 힙(Heap) 메모리를 2GB로 설정하면 어떻게 될까요? JVM 자체 오버헤드(Metaspace, Stack, Native Memory) 때문에 전체 프로세스 사용량은 2GB를 넘게 되고, 결국 OOMKilled 당합니다. (컨테이너 메모리 > 힙 메모리 + 오버헤드여야 합니다)

디버깅 가이드: 범인을 잡자

1. kubectl describe pod 확인하기

가장 먼저 해야 할 일입니다. 파드의 상태를 확인하세요.

Terminal window
kubectl describe pod <pod-name>

State: Terminated, Reason: OOMKilled가 찍혀 있다면 메모리 부족이 확실합니다.

2. 모니터링 그래프 보기 (Prometheus/Grafana)

메모리 사용량 그래프의 기울기를 유심히 살펴보세요.

  • 계단식으로 꾸준히 우상향: 100% 메모리 누수입니다. 배포 직후부터 서서히 차오르다가 한계선에 도달하면 죽습니다.
  • 갑자기 수직 상승: 트래픽 스파이크나 대용량 데이터 처리 로직 문제입니다. 로그와 대조해서 그 시간에 호출된 API를 찾아야 합니다.

3. 언어별 프로파일링 (Profiling)

메모리 누수가 의심된다면 코드를 들여다봐야 합니다.

  • Go: pprof를 사용하여 힙 프로파일(Heap Profile)을 분석합니다. 어떤 함수가 메모리를 점유 중인지 나옵니다.
  • Java: Heap Dump를 생성하여 Eclipse MAT 같은 도구로 분석합니다.
  • Node.js: Chrome DevTools의 Memory 탭이나 heapdump 라이브러리를 사용합니다.

해결책: 독을 수리하는 법

  1. 메모리 누수 고치기: 프로파일링 결과로 찾은 누수 지점을 수정합니다. 불필요한 객체 참조를 끊거나, 캐시 만료(TTL) 설정을 추가합니다.
  2. 데이터 처리 방식 변경: 대용량 데이터는 한 번에 로딩하지 말고, **스트림(Stream)**이나 페이지네이션(Pagination) 방식으로 나누어 처리하도록 로직을 변경합니다.
  3. 적절한 Limit 설정: 애플리케이션의 평소 사용량(Request)과 최대 사용량(Limit)을 모니터링 데이터를 기반으로 재설정합니다. 일반적으로 limits를 requests의 1.2~1.5배 정도로 설정하는 것이 안전합니다.
  4. HPA (Horizontal Pod Autoscaler) 적용: 메모리 사용량이 70~80%에 도달하면 파드 개수를 늘려서 부하를 분산시키도록 설정합니다.

마치며

OOMKilled는 시스템이 보내는 구조신호입니다. “메모리 더 줘!”라고 떼쓰는 게 아니라, “나 지금 뭔가 잘못됐어, 빨리 고쳐줘!”라고 외치는 것입니다. 이 신호를 무시하고 리미트만 늘리는 건, 시한폭탄의 타이머를 조금 늦추는 미봉책일 뿐입니다.

오늘 밤, 여러분의 파드가 안녕한지 모니터링 대시보드를 한 번 확인해 보세요. 우상향 하는 그래프가 보인다면, 바로 지금이 디버깅을 시작할 때입니다.

함께 읽을 실전 가이드

Docker 환경에서는 Exit Code 137과 OOM·강제 종료 구분을 참고하세요. 종료 코드만으로 원인을 확정하지 않고 상태·시간·로그를 대조합니다.

← 전체 글로 돌아가기