- Published on
EKS Pod CrashLoopBackOff? OOMKilled 진단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡해 보이는데도 EKS에서 특정 Pod가 계속 재시작하며 CrashLoopBackOff 상태로 빠지는 경우가 있습니다. 그중 가장 흔한 원인 중 하나가 OOMKilled(Out Of Memory Killed)입니다. 문제는 애플리케이션 로그만 보면 아무 단서 없이 “갑자기 죽은” 것처럼 보인다는 점입니다.
이 글에서는 다음을 목표로 합니다.
CrashLoopBackOff와OOMKilled를 구분하고, 어떤 순서로 확인해야 가장 빠른지- Pod 이벤트, 컨테이너 종료 사유, 메모리 사용량, 노드 OOM 로그까지 “증거”를 연결하는 방법
- 단순히 requests/limits를 올리는 처방이 아니라, 재발을 줄이는 운영 관점의 수정 포인트
관련해서 노드 레벨 OOM 로그까지 더 깊게 파고들고 싶다면 리눅스 OOM Killer로 프로세스 죽음 원인 추적도 함께 보면 좋습니다.
1) CrashLoopBackOff와 OOMKilled의 관계
CrashLoopBackOff는 “컨테이너가 반복적으로 실패해서 Kubernetes가 재시작 백오프(지수적 대기)를 걸고 있는 상태”입니다.OOMKilled는 “컨테이너가 메모리 제한(limit)을 넘겨 커널/런타임에 의해 강제 종료된 종료 사유(reason)”입니다.
즉, OOMKilled는 원인(Reason)이고, CrashLoopBackOff는 결과(상태)일 수 있습니다.
또 한 가지 중요한 점:
- 컨테이너가
exit code137로 끝나면 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: CrashLoopBackOffLast State: Terminated/Reason: OOMKilledExit Code: 137
예시(핵심만 발췌):
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
여기서 Last State가 OOMKilled로 찍히면 “컨테이너 메모리 limit 초과로 강제 종료”가 거의 확정입니다.
2.2 이벤트에서 메모리 관련 단서 확인
kubectl -n <namespace> get events --sort-by=.lastTimestamp | tail -n 50
이벤트에는 다음 유형이 보일 수 있습니다.
Back-off restarting failed containerKilling 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에서 MemoryPressure가 True였는지 확인합니다.
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가지 축입니다.
- limit 상향(단기 처방)
- 메모리 사용 최적화(근본 처방)
- 트래픽/동시성/버퍼링 구조 변경(운영 처방)
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) 실전 진단 루틴(요약)
아래 순서대로 하면 대부분의 케이스를 빠르게 수습할 수 있습니다.
kubectl describe pod로Last Statereason 확인(OOMKilled인지)kubectl logs --previous로 죽기 직전 로그 확인- Deployment의
resources.requests/limits확인(특히 memory limit) kubectl top pod --containers및 모니터링에서 메모리 스파이크 확인- 노드
MemoryPressure및 노드 OOM 로그 여부 확인 - 단기: limit 조정 및 롤아웃
- 중기: 런타임 힙/버퍼/동시성 조정, HPA 기준 보완
- 장기: 누수/대용량 처리 구조 개선 및 프로파일링 자동화
10) 같이 보면 좋은 EKS 운영 글
EKS에서 장애를 진단할 때는 한 가지 증상만 보지 말고, 인그레스/네트워크/상태 드리프트 같은 주변 이슈도 함께 점검하는 것이 좋습니다.
- Terraform로 EKS 운영 중 상태가 꼬여 배포가 이상해졌다면: Terraform EKS 상태 꼬임으로 apply 무한 반복 끊기
- 애플리케이션은 정상인데 Ingress에서 500이 튄다면: EKS ALB Ingress 500 Target reset 원인·해결
- 특정 요청에서 헤더가 커져 장애가 난다면: EKS에서 HTTP 431 해결 - 헤더·쿠키 과다 진단
CrashLoopBackOff는 현상이고, OOMKilled는 매우 흔한 원인 중 하나입니다. 핵심은 “Pod 종료 사유(Last State) → 이전 로그 → 리소스 제한 → 실제 사용량(시계열) → 노드 상태”로 증거를 연결해, 단순 증상 완화가 아니라 재발 방지까지 이어지게 만드는 것입니다.