Published on

EKS Pod CrashLoopBackOff? OOMKilled 진단법

Authors

서버가 멀쩡해 보이는데도 EKS에서 특정 Pod가 계속 재시작하며 CrashLoopBackOff 상태로 빠지는 경우가 있습니다. 그중 가장 흔한 원인 중 하나가 OOMKilled(Out Of Memory Killed)입니다. 문제는 애플리케이션 로그만 보면 아무 단서 없이 “갑자기 죽은” 것처럼 보인다는 점입니다.

이 글에서는 다음을 목표로 합니다.

  • CrashLoopBackOffOOMKilled를 구분하고, 어떤 순서로 확인해야 가장 빠른지
  • Pod 이벤트, 컨테이너 종료 사유, 메모리 사용량, 노드 OOM 로그까지 “증거”를 연결하는 방법
  • 단순히 requests/limits를 올리는 처방이 아니라, 재발을 줄이는 운영 관점의 수정 포인트

관련해서 노드 레벨 OOM 로그까지 더 깊게 파고들고 싶다면 리눅스 OOM Killer로 프로세스 죽음 원인 추적도 함께 보면 좋습니다.

1) CrashLoopBackOff와 OOMKilled의 관계

  • CrashLoopBackOff는 “컨테이너가 반복적으로 실패해서 Kubernetes가 재시작 백오프(지수적 대기)를 걸고 있는 상태”입니다.
  • OOMKilled는 “컨테이너가 메모리 제한(limit)을 넘겨 커널/런타임에 의해 강제 종료된 종료 사유(reason)”입니다.

즉, OOMKilled는 원인(Reason)이고, CrashLoopBackOff는 결과(상태)일 수 있습니다.

또 한 가지 중요한 점:

  • 컨테이너가 exit code 137로 끝나면 OOM 가능성이 매우 높습니다(항상 100%는 아니지만 실무에선 강한 시그널).

2) 1분 컷: kubectl로 OOMKilled 여부 확인

가장 먼저 할 일은 “마지막 종료 사유”를 확인하는 것입니다.

2.1 Pod 상태와 마지막 종료 사유 보기

kubectl -n <namespace> get pod <pod-name> -o wide
kubectl -n <namespace> describe pod <pod-name>

describe 출력에서 다음을 찾습니다.

  • State: Waiting / Reason: CrashLoopBackOff
  • Last State: Terminated / Reason: OOMKilled
  • Exit Code: 137

예시(핵심만 발췌):

State:          Waiting
  Reason:       CrashLoopBackOff
Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137

여기서 Last StateOOMKilled로 찍히면 “컨테이너 메모리 limit 초과로 강제 종료”가 거의 확정입니다.

2.2 이벤트에서 메모리 관련 단서 확인

kubectl -n <namespace> get events --sort-by=.lastTimestamp | tail -n 50

이벤트에는 다음 유형이 보일 수 있습니다.

  • Back-off restarting failed container
  • Killing container with id ... (직접적으로 OOM을 말하지 않더라도 힌트)

3) 로그 확인: “죽기 직전” 로그를 반드시 본다

CrashLoop 상황에서는 현재 컨테이너 로그보다 “이전(바로 직전 실행)” 로그가 중요합니다.

kubectl -n <namespace> logs <pod-name> -c <container-name> --previous

OOMKilled는 프로세스가 SIGKILL로 즉시 종료되는 경우가 많아 애플리케이션이 정상적인 종료 로그를 남기지 못합니다. 하지만 다음과 같은 간접 징후가 남을 수 있습니다.

  • 대량 처리 직전까지는 정상 로그가 찍히다가 갑자기 끊김
  • 특정 요청/배치/스케줄러 작업 시작 직후 로그가 끊김
  • JVM/Node/Python 등 런타임이 메모리 부족 경고를 남기다 종료

4) 메모리 “limit”과 “requests”를 분리해서 해석하기

OOMKilled는 기본적으로 컨테이너의 resources.limits.memory를 넘겼을 때 발생합니다.

  • requests.memory: 스케줄링 기준(이만큼은 필요하다고 선언)
  • limits.memory: 상한선(이 이상 쓰면 죽을 수 있음)

흔한 함정:

  • requests만 올리고 limits를 그대로 두면, 여전히 OOMKilled는 발생합니다.
  • limits만 크게 올리면 노드 전체 메모리를 압박해 “노드 OOM”으로 번질 수 있습니다.

4.1 현재 리소스 설정 확인

kubectl -n <namespace> get deploy <deploy-name> -o yaml | sed -n '1,200p'

resources 섹션을 확인합니다.

resources:
  requests:
    cpu: "200m"
    memory: "256Mi"
  limits:
    cpu: "1"
    memory: "512Mi"

5) metrics-server로 “실제 사용량” 확인

클러스터에 metrics-server가 있다면 현재 사용량을 빠르게 볼 수 있습니다.

kubectl -n <namespace> top pod <pod-name>
kubectl -n <namespace> top pod --containers | grep <pod-name>

주의할 점:

  • top는 “현재 시점” 사용량이므로, 스파이크(급증)로 죽는 워크로드는 죽기 직전 사용량을 못 볼 수 있습니다.
  • 그럴 때는 Prometheus/Grafana(또는 CloudWatch Container Insights)에서 시계열로 확인해야 합니다.

6) 노드 레벨 OOM인지, 컨테이너 limit OOM인지 구분

OOMKilled가 찍혔다면 보통 컨테이너 limit 초과입니다. 하지만 다음 상황에서는 노드 메모리 압박이 원인일 수도 있습니다.

  • 여러 Pod가 동시에 메모리를 많이 먹고 노드 전체가 메모리 부족
  • limits를 설정하지 않았거나 너무 크게 잡아 노드가 버티지 못함

6.1 노드 상태에서 MemoryPressure 확인

kubectl describe node <node-name>

Conditions에서 MemoryPressureTrue였는지 확인합니다.

6.2 (권한이 된다면) 노드 dmesg/journal에서 OOM 로그 확인

관리형 노드 그룹/자체 AMI/SSM 접근이 가능하다면 노드에서 다음을 확인합니다.

sudo dmesg -T | grep -i -E 'oom|out of memory|killed process' | tail -n 50

노드 OOM 분석은 케이스가 다양하므로, 더 깊게는 리눅스 OOM Killer로 프로세스 죽음 원인 추적의 흐름(oom_score_adj, victim 선정, 로그 해석)을 그대로 적용할 수 있습니다.

7) CrashLoopBackOff인데 OOMKilled가 아닐 때 체크리스트

CrashLoopBackOff가 항상 OOM은 아닙니다. describe pod에서 Last State reason이 다음 중 무엇인지도 같이 보세요.

  • Error: 앱이 예외로 종료(설정/환경변수/의존성 문제)
  • Completed: Job/원샷 프로세스인데 Deployment로 띄운 설계 문제
  • ContainerCannotRun: 엔트리포인트/권한/파일 경로 문제
  • ImagePullBackOff: 이미지 pull 실패(네트워크/권한/태그)

그래도 exit code 137이 보이면 OOM 가능성을 다시 의심해야 합니다.

8) 재발 방지: 단순 증상 완화가 아닌 “원인 제거”

OOMKilled를 없애는 방법은 크게 3가지 축입니다.

  1. limit 상향(단기 처방)
  2. 메모리 사용 최적화(근본 처방)
  3. 트래픽/동시성/버퍼링 구조 변경(운영 처방)

8.1 limit만 올리기 전에 반드시 확인할 것

  • 메모리 사용이 “선형 증가”인지(누수) vs “순간 스파이크”인지(버퍼/배치)
  • 특정 요청 패턴에서만 터지는지(대용량 파일, 큰 JSON, 압축 해제)
  • HPA가 CPU만 보고 스케일링해서 메모리 병목을 방치하는지

8.2 예시: 리소스 설정 조정 패치

kubectl -n <namespace> patch deploy <deploy-name> --type='json' -p='[
  {"op":"replace","path":"/spec/template/spec/containers/0/resources/requests/memory","value":"512Mi"},
  {"op":"replace","path":"/spec/template/spec/containers/0/resources/limits/memory","value":"1024Mi"}
]'

운영 팁:

  • requests를 너무 낮게 두면 노드에 빽빽하게 스케줄되어 “노드 OOM” 위험이 커집니다.
  • limits를 너무 높게 두면 한 Pod가 노드 메모리를 독점할 수 있습니다.

8.3 JVM/Node.js 런타임은 “앱 메모리”와 “컨테이너 메모리”가 다르다

특히 JVM/Node.js는 컨테이너 limit을 고려하지 않으면 런타임이 사용할 힙/Old Space를 과하게 잡아 OOMKilled로 이어질 수 있습니다.

  • Java: -XX:MaxRAMPercentage 또는 -Xmx를 컨테이너 limit에 맞게 설정
  • Node.js: --max-old-space-size 고려

예: Node.js 컨테이너가 512Mi limit이면, old space를 400MB로 잡는 식의 보수적 설정이 필요할 수 있습니다(네이티브/버퍼/런타임 오버헤드 고려).

env:
  - name: NODE_OPTIONS
    value: "--max-old-space-size=400"

8.4 메모리 누수 의심 시: “재시작으로 숨기지 말고” 증거를 남겨라

  • heap dump, pprof, tracemalloc 등 언어별 프로파일링을 켭니다.
  • OOM 직전 상태를 잡기 위해 readiness/liveness를 과도하게 공격적으로 두지 않습니다.
  • 트래픽이 몰리는 시간대에만 터지면, 부하 테스트로 재현 후 비교합니다.

9) 실전 진단 루틴(요약)

아래 순서대로 하면 대부분의 케이스를 빠르게 수습할 수 있습니다.

  1. kubectl describe podLast State reason 확인(OOMKilled인지)
  2. kubectl logs --previous로 죽기 직전 로그 확인
  3. Deployment의 resources.requests/limits 확인(특히 memory limit)
  4. kubectl top pod --containers 및 모니터링에서 메모리 스파이크 확인
  5. 노드 MemoryPressure 및 노드 OOM 로그 여부 확인
  6. 단기: limit 조정 및 롤아웃
  7. 중기: 런타임 힙/버퍼/동시성 조정, HPA 기준 보완
  8. 장기: 누수/대용량 처리 구조 개선 및 프로파일링 자동화

10) 같이 보면 좋은 EKS 운영 글

EKS에서 장애를 진단할 때는 한 가지 증상만 보지 말고, 인그레스/네트워크/상태 드리프트 같은 주변 이슈도 함께 점검하는 것이 좋습니다.


CrashLoopBackOff는 현상이고, OOMKilled는 매우 흔한 원인 중 하나입니다. 핵심은 “Pod 종료 사유(Last State) → 이전 로그 → 리소스 제한 → 실제 사용량(시계열) → 노드 상태”로 증거를 연결해, 단순 증상 완화가 아니라 재발 방지까지 이어지게 만드는 것입니다.