본문으로 건너뛰기
메모리가 가득 차서 터지기 직전인 컨테이너 일러스트

# Kubernetes OOMKilled: 내 파드가 자꾸 죽는 진짜 이유 (Exit Code 137)

Table of Contents

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

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

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

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

파드가 OOMKilled 되었다는 건, 쿠버네티스가 임의로 죽인 것이 아닙니다. 정확히는 리눅스 커널의 OOM Killer가 “이 컨테이너가 할당된 메모리 한계(cgroup limit)를 넘었네? 시스템 전체를 살리기 위해 널 죽여야겠다.” 하고 SIGKILL (9) 시그널을 보낸 것입니다. Exit Code 137128 + 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 확인하기

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

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)을 모니터링 데이터를 기반으로 재설정합니다. 일반적으로 limitsrequests의 1.2~1.5배 정도로 설정하는 것이 안전합니다.
  4. HPA (Horizontal Pod Autoscaler) 적용: 메모리 사용량이 70~80%에 도달하면 파드 개수를 늘려서 부하를 분산시키도록 설정합니다.

마치며

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

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

이 글 공유하기:
My avatar

글을 마치며

이 글이 도움이 되었기를 바랍니다. 궁금한 점이나 의견이 있다면 댓글로 남겨주세요.

더 많은 기술 인사이트와 개발 경험을 공유하고 있으니, 다른 포스트도 확인해보세요.

유럽살며 여행하며 코딩하는 노마드의 여정을 함께 나누며, 함께 성장하는 개발자 커뮤니티를 만들어가요! 🚀


관련 포스트

# Connection Pool 고갈 완벽 디버깅 가이드: The Silent Killer 잡기

게시:

프로덕션 환경에서 발생하는 데이터베이스 Connection Pool 고갈 문제를 완벽하게 해결하는 실전 가이드입니다. HikariCP, Sequelize, ADO.NET 등 주요 프레임워크별 디버깅 전략, 연결 누수 탐지 방법, 그리고 2025년 최신 모니터링 및 예방 기법까지 모두 다룹니다.

읽기

# AI 코딩 어시스턴트 디버깅 실패 완벽 대응 가이드: Claude, GPT-4, Copilot 프로덕션 트러블슈팅

게시:

2025년 Microsoft 연구 결과, AI 모델들이 실제 소프트웨어 디버깅에서 여전히 한계를 보입니다. Claude 3.7, GPT-4o, Copilot의 디버깅 실패 사례와 실전 해결 전략을 상세히 알려드립니다. SWE-bench 벤치마크 분석부터 프로덕션 환경 대응법까지.

읽기