- Published on
EKS Pod CrashLoopBackOff? OOMKilled 5분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 중인 EKS에서 Pod가 갑자기 CrashLoopBackOff로 빠지면 대부분은 “앱이 죽었다”로만 끝나기 쉽습니다. 하지만 실제로는 컨테이너 프로세스가 스스로 종료한 것인지, 혹은 커널 OOM Killer가 메모리를 회수하려고 강제 종료한 것인지에 따라 대응이 완전히 달라집니다.
이 글은 “5분 안에” OOMKilled 여부를 판별하고, 원인 좁히기부터 즉시 완화, 재발 방지까지 이어지는 실전 루틴을 제공합니다.
이미 CrashLoopBackOff 일반 디버깅(로그, Probe, 리소스, 이벤트)을 정리한 글이 있다면 아래도 같이 보세요.
- Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅
- (스케줄링 자체가 안 되는 케이스라면) K8s Pod Pending(0/노드) - 스케줄 불가 원인·해결
0. 5분 진단 목표: “OOMKilled인가?”를 먼저 확정
CrashLoopBackOff는 결과일 뿐이고, 가장 먼저 확정해야 할 것은 아래 둘 중 무엇인지입니다.
- OOMKilled: 컨테이너가 메모리 제한(
limits.memory)을 넘겨 커널에 의해 강제 종료됨 - Non-OOM: 앱 예외, 설정 오류, 의존성 장애, Probe 실패 등으로 프로세스가 종료됨
OOMKilled는 보통 종료 코드 137(SIGKILL)과 함께 나타납니다. 다만 137이 항상 OOM을 의미하진 않으니, Reason: OOMKilled를 함께 확인해야 합니다.
1. 60초: Pod 상태에서 OOMKilled 확인
가장 빠른 방법은 describe에서 마지막 종료 상태를 보는 것입니다.
# 네임스페이스와 파드 이름을 알고 있다는 가정
kubectl -n <namespace> describe pod <pod-name>
여기서 아래 블록을 찾습니다.
Last State: TerminatedReason: OOMKilledExit Code: 137
또는 JSONPath로 바로 뽑아도 됩니다.
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
여러 컨테이너가 있는 Pod라면 특정 컨테이너를 지정해 확인하세요.
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'
2. 60초: 이벤트에서 “메모리 압박” 흔적 확인
OOMKilled는 보통 이벤트에도 흔적이 남습니다.
kubectl -n <namespace> get events --sort-by=.lastTimestamp | tail -n 50
다음 키워드를 눈여겨 보세요.
OOMKilledKilling또는Killed(컨테이너 종료)- 노드 측면의
MemoryPressure
단, 이벤트는 보존 기간이 짧거나(클러스터 설정에 따라) 다른 이벤트에 묻힐 수 있어, 최종 판정은 Pod Last State가 더 확실합니다.
3. 90초: “죽기 직전 로그”를 확보한다
CrashLoopBackOff에서 가장 흔한 실수는 “현재 떠 있는 컨테이너 로그”만 보고 끝내는 것입니다. 재시작 루프에서는 이전 인스턴스 로그가 핵심입니다.
kubectl -n <namespace> logs <pod-name> --previous
멀티 컨테이너 Pod라면 컨테이너를 지정하세요.
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous
OOMKilled인 경우, 애플리케이션 로그는 아무 말 없이 끊기는 경우가 많습니다. 반대로 앱 예외라면 스택 트레이스, 설정 누락, DB 연결 실패 같은 명확한 증상이 남습니다.
4. 60초: requests/limits가 현실적인지 확인
OOMKilled의 1차 원인은 대부분 리소스 설정입니다.
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}requests: {.resources.requests.memory}{"\n"}limits: {.resources.limits.memory}{"\n\n"}{end}'
여기서 빠르게 판단합니다.
limits.memory가 너무 낮다: 실제 피크 메모리보다 작으면 OOMKilled는 시간문제requests.memory가 너무 낮다: 스케줄링은 되지만 노드에서 경쟁이 심해져 성능 저하 및 간접적인 장애 유발- limit이 아예 없다: 컨테이너가 노드 메모리를 과점해 노드 전체가 흔들릴 수 있음
5. 60초: 노드 메모리 압박 여부 확인
컨테이너 limit OOM과 별개로, 노드가 메모리 압박 상태면 다른 Pod까지 연쇄적으로 불안정해질 수 있습니다.
kubectl describe node <node-name>
Conditions에서 아래를 확인합니다.
MemoryPressure: True
또한 Metrics Server가 있다면 빠르게 Top을 찍어봅니다.
kubectl top pod -n <namespace> | head
kubectl top node | head
kubectl top이 안 된다면(미설치) 그 자체가 관측 공백이므로, 운영 클러스터라면 Metrics Server 또는 Prometheus 계열을 반드시 붙이는 것을 권장합니다.
6. OOMKilled로 확정되면: 즉시 완화 3가지
6.1 가장 빠른 완화: 메모리 limit 상향
원인 분석 전에 서비스부터 살려야 한다면 limit을 올리는 것이 가장 빠릅니다.
kubectl -n <namespace> set resources deployment/<deploy-name> -c <container-name> --limits=memory=1024Mi --requests=memory=512Mi
주의할 점
- limit만 올리고 request를 그대로 두면, 노드에 “싸게” 스케줄되어 실제로는 메모리가 부족한 노드에 몰릴 수 있습니다.
- 반대로 request까지 과하게 올리면 스케줄이 안 될 수 있으니, 현재 노드 여유와 함께 조정합니다.
6.2 HPA가 있다면 스케일 아웃으로 피크 완화
메모리 누수라기보다 요청량 폭증으로 힙이 커지는 타입이라면, 레플리카를 늘려 피크를 분산시킬 수 있습니다.
kubectl -n <namespace> scale deployment/<deploy-name> --replicas=6
다만 레플리카 증가가 곧 메모리 총량 증가이므로, 노드 여유가 없으면 Pending이 날 수 있습니다.
6.3 노드가 부족하면 Cluster Autoscaler 또는 노드 증설
EKS에서 워크로드가 늘었는데 노드가 그대로면, limit 상향이나 스케일 아웃 모두 결국 막힙니다. 이 경우는 애플리케이션 문제가 아니라 “수용 능력” 문제일 수 있습니다.
7. 재발 방지: “왜 메모리가 늘었나”를 유형별로 쪼갠다
OOMKilled는 결과이고, 원인은 크게 4가지로 나뉩니다.
7.1 정상 동작인데 limit이 비현실적으로 낮다
가장 흔합니다. 특히 Java, Node.js, Python 계열에서 기본 메모리 전략이 컨테이너 limit과 충돌할 수 있습니다.
- Java: 컨테이너 인식 옵션, 힙 상한 설정 필요
- Node.js: V8 old space 제한 조정 필요
Node.js 예시
# 예: old space를 768MB로 제한
node --max-old-space-size=768 server.js
Kubernetes manifest에서는 환경변수로 넣는 형태가 많습니다.
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=768"
7.2 트래픽 급증으로 순간 피크가 limit을 넘는다
이 경우는 메모리 누수라기보다 “버퍼가 커지는” 패턴이 많습니다.
- 대용량 응답을 메모리에 한 번에 올림
- 압축, 이미지 처리, PDF 생성 같은 작업이 동시 실행
- 큐 적체로 인한 in-memory backlog
대응
- 동시성 제한(워크 큐, 스레드풀, 세마포어)
- 스트리밍 처리로 변경(파일 업로드, 다운로드)
- 요청 크기 제한
7.3 메모리 누수 또는 캐시 무한 성장
증상
- 시간이 지날수록 RSS가 계단식으로 증가
- 트래픽이 낮아져도 메모리가 회수되지 않음
대응
- 힙 덤프 또는 프로파일링
- 캐시 TTL, max size 설정
- 라이브러리 누수 이슈 확인
7.4 Probe 설정이 잘못되어 “정상인데 재시작”되는 경우
간혹 OOMKilled로 오인하는 케이스가 있습니다. 예를 들어 readiness가 계속 실패해 트래픽이 안 들어오고, liveness가 과격해 재시작을 유발하면 CrashLoopBackOff가 됩니다.
이 경우는 OOMKilled가 아니라 Reason이 다르게 찍히는 경우가 많습니다. Probe 디버깅은 아래 글의 체크리스트가 도움이 됩니다.
8. EKS에서 특히 자주 겪는 함정 4가지
8.1 사이드카가 메모리를 다 먹는다
서비스 컨테이너가 아니라 Envoy, Fluent Bit, Datadog Agent 같은 사이드카가 OOMKilled 나는 경우가 있습니다. 위에서 소개한 JSONPath로 컨테이너별 Reason을 꼭 확인해야 합니다.
8.2 로그 폭주로 메모리가 늘어나는 패턴
애플리케이션이 과도한 로그를 찍으면, 로깅 드라이버나 사이드카가 압박을 받을 수 있습니다. 장애 시점에만 로그가 폭증하는지도 함께 봅니다.
8.3 스팟 노드 혼용 시 “불안정”을 OOM으로 착각
스팟 중단은 OOMKilled가 아니라 노드 종료 이벤트로 나타납니다. Pod가 다른 노드로 이동하면서 재시작이 반복되는 것처럼 보일 수 있으니, 노드 이벤트와 함께 확인합니다.
8.4 HPA 기준이 CPU만 있으면 메모리 병목을 못 잡는다
CPU는 여유인데 메모리만 터지는 워크로드가 많습니다. 가능하면 메모리 기반 HPA(또는 KEDA, 커스텀 메트릭)를 함께 고려하세요.
9. “5분 루틴” 체크리스트 요약
아래 순서대로 하면, 대부분의 케이스는 5분 내로 방향이 잡힙니다.
kubectl describe pod <pod-name>에서Reason: OOMKilled확인kubectl logs --previous로 죽기 직전 로그 확보requests/limits확인 후 너무 낮으면 즉시 상향kubectl top pod또는 노드MemoryPressure로 주변 영향 확인- 완화: limit 상향, 스케일 아웃, 노드 증설 중 하나 선택
- 재발 방지: 피크인지, 누수인지, 사이드카인지 유형 분류 후 설정/코드 개선
10. 부록: 현장용 커맨드 묶음
네임스페이스에서 CrashLoopBackOff Pod만 빠르게 찾기
kubectl -n <namespace> get pod | grep -E 'CrashLoopBackOff|Error'
Pod의 컨테이너별 마지막 종료 사유 한 번에 보기
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'
이전 로그 확인
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous
Deployment 리소스 빠른 조정
kubectl -n <namespace> set resources deployment/<deploy-name> -c <container-name> --requests=memory=512Mi --limits=memory=1024Mi
CrashLoopBackOff는 “현상”이고, OOMKilled는 그중에서도 특히 **재현이 어렵고(트래픽 피크), 로그가 남지 않으며(강제 종료), 주변에 연쇄 영향(노드 압박)**을 주기 쉬운 유형입니다. 위 루틴대로 먼저 OOMKilled인지 확정한 뒤, 리소스 설정과 워크로드 특성(피크, 누수, 사이드카)을 분리해 접근하면 진단 속도가 확 올라갑니다.