- Published on
EKS CrashLoopBackOff? OOMKilled 진단 7단계
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CrashLoopBackOff는 원인이 매우 다양하지만, EKS 운영에서 가장 자주 만나는 축은 OOMKilled(메모리 부족으로 커널이 프로세스를 강제 종료)입니다. 특히 애플리케이션 자체는 정상인데도 특정 트래픽/배포 이후 갑자기 재시작 루프에 빠지면, 대개 리소스 설정(requests/limits)과 런타임 메모리 사용량의 불일치가 방아쇠가 됩니다.
이 글은 “지금 당장 원인을 좁혀서 조치”하는 데 초점을 맞춰, CrashLoopBackOff 상황에서 OOMKilled를 7단계로 진단하는 체크리스트를 제공합니다. 마지막에는 재발 방지(리소스·GC·HPA·노드 전략)까지 연결합니다.
관련해서 애플리케이션 레벨 OOM 원인 분석이 필요하면 Spring Boot OOM 원인추적과 힙덤프 분석 실전도 함께 보면 좋습니다.
0. CrashLoopBackOff와 OOMKilled를 구분해야 하는 이유
CrashLoopBackOff는 “컨테이너가 종료되고 재시작을 반복한다”는 상태일 뿐입니다. 종료 원인은 크게 다음으로 갈립니다.
- 애플리케이션 예외로 프로세스가 종료(Exit Code
1등) - 라이브니스 프로브 실패로 kubelet이 강제 재시작
- 이미지/엔트리포인트/권한 문제로 즉시 종료
- 노드/컨테이너 런타임 문제
- 그리고
OOMKilled(Exit Code137또는 ReasonOOMKilled)
OOMKilled는 재현이 어려울 수 있고(트래픽/데이터/캐시 상태에 의존), 단순히 limits만 올려서 “임시 해결”하면 비용과 안정성 문제가 같이 커집니다. 따라서 원인을 “정확히” 확인하고, 필요한 최소 조치로 안정화하는 것이 중요합니다.
1단계: 컨테이너 종료 Reason과 Exit Code를 먼저 확정
가장 먼저 “정말 OOMKilled인가?”를 확정합니다. kubectl describe pod의 Last State와 State를 확인하세요.
kubectl -n <namespace> describe pod <pod-name>
확인 포인트:
Last State: TerminatedReason: OOMKilled이면 거의 확정Exit Code: 137도 OOM 가능성이 큼(시그널SIGKILL)
Reason: Error+Exit Code: 1이면 애플리케이션 크래시 가능성이 큼
컨테이너가 여러 개면(사이드카 포함) 어떤 컨테이너가 죽는지 반드시 구분해야 합니다. 예를 들어 Envoy/istio-proxy가 OOM이면 앱 로그만 봐서는 원인을 놓칩니다.
추가로, 재시작 횟수와 최근 종료 시간을 함께 확인합니다.
kubectl -n <namespace> get pod <pod-name> -o wide
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{.status.containerStatuses[*].restartCount}'
2단계: 이벤트에서 메모리 압박 신호를 찾기
OOM은 “컨테이너 메모리 limit 초과”와 “노드 메모리 압박” 두 갈래가 있습니다. 이벤트는 둘을 구분하는 데 도움을 줍니다.
kubectl -n <namespace> get events --sort-by='.lastTimestamp'
자주 보이는 패턴:
Killing/OOMKilled관련 메시지Evicted또는The node was low on resource: memory.Back-off restarting failed container
노드 메모리 압박이 의심되면 노드 이벤트도 확인합니다.
kubectl describe node <node-name>
여기서 MemoryPressure가 True로 뜨거나, Non-terminated Pods의 합이 노드 메모리를 과도하게 점유하면 “컨테이너 limit”을 올리는 것만으로는 해결되지 않을 수 있습니다.
3단계: 이전 컨테이너 로그를 확보(현재 로그만 보면 놓친다)
CrashLoopBackOff에서는 “이미 죽은 직전 실행”의 로그가 핵심입니다. 반드시 --previous를 붙여 확인합니다.
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous
로그에서 다음을 찾습니다.
- OOM 직전 메모리 급증을 암시하는 패턴(캐시 워밍, 대량 배치, 특정 요청)
- JVM/Node/Python 런타임의 메모리 관련 경고
- 프로브 실패로 인한 종료(예: readiness/liveness 실패 로그)
만약 로그가 거의 없고 “갑자기 죽는다”면 OOM일 가능성이 더 커집니다. SIGKILL은 애플리케이션이 정상 종료 로그를 남길 시간을 주지 않습니다.
4단계: requests/limits 설정과 QoS 클래스를 점검
OOM 진단에서 가장 흔한 실수는 “limit이 너무 낮거나, request가 너무 낮아 노드에서 밀집 배치되는 상황”을 놓치는 것입니다.
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}{.resources}{"\n\n"}{end}'
핵심 체크:
resources.limits.memory가 실제 피크 사용량보다 낮지 않은가resources.requests.memory가 너무 낮아 노드에 과밀 스케줄링되지 않는가- QoS 클래스
Guaranteed: requests와 limits가 동일Burstable: requests와 limits가 다름(대부분)BestEffort: 둘 다 없음(운영 환경에서 위험)
QoS는 축출(eviction) 우선순위에도 영향을 줍니다. 노드가 메모리 압박일 때 BestEffort가 가장 먼저 축출됩니다.
실무 팁:
- “일단 limit만 올리기” 전에,
request를 현실적으로 맞춰서 과밀 배치를 줄이는 것이 재발 방지에 더 효과적인 경우가 많습니다.
5단계: 실제 메모리 사용량을 숫자로 확인(피크 기준)
원인 추정이 아니라 “실제 사용량”을 봐야 합니다. Metrics Server가 있다면 빠르게 볼 수 있습니다.
kubectl -n <namespace> top pod <pod-name>
kubectl -n <namespace> top pod --containers | grep <pod-name>
다만 top은 “현재” 사용량이라, 이미 죽어버린 피크를 놓칠 수 있습니다. 그래서 다음을 병행하세요.
- Prometheus/Grafana에서 컨테이너 메모리 시계열 확인
container_memory_working_set_bytescontainer_memory_rss
- CloudWatch Container Insights를 쓰는 경우 해당 지표 확인
피크가 limit에 닿는 순간이 있는지 확인한 뒤, “피크가 정상 트래픽에서 발생하는지”와 “특정 요청/배치에서만 발생하는지”를 분리해야 합니다.
6단계: OOM의 종류를 분기(컨테이너 limit vs 노드 압박)
여기서부터는 조치가 달라집니다.
A. 컨테이너 limit OOM(가장 흔함)
특징:
describe pod에Reason: OOMKilled- 노드
MemoryPressure는False일 수 있음 - 특정 컨테이너만 반복적으로 죽음
조치 방향:
- limit 상향(단, 근본 원인 분석과 함께)
- 애플리케이션 메모리 상한 설정
- JVM:
-Xmx가 컨테이너 limit보다 과도하지 않게 - Node.js:
--max-old-space-size
- JVM:
- 캐시/버퍼 크기 제한
- 대량 배치/초기 로딩을 쪼개기
B. 노드 메모리 압박에 따른 축출/불안정
특징:
- 이벤트에
Evicted또는 노드MemoryPressure: True - 여러 파드가 연쇄적으로 재시작
- 새 파드가 스케줄링/기동에 실패
조치 방향:
- 노드 인스턴스 타입 상향 또는 노드 수 증설
- 과밀 배치 완화:
requests.memory상향, HPA/Cluster Autoscaler 조정 - 메모리 많이 먹는 워크로드를 노드풀 분리(taint/toleration)
EKS에서 노드/파드 간 통신 문제도 CrashLoopBackOff의 원인이 될 수 있으니, 네트워크/인증서 계열 증상이 섞여 보이면 EKS Pod간 TLS 실패? 인증서·SNI·mTLS 10분 진단처럼 다른 축도 같이 배제하는 것이 좋습니다.
7단계: 재발 방지용 “안전장치”를 설계(관측·배포·스케일)
원인을 찾았더라도, 운영에서는 “다시 터져도 빨리 감지하고 자동 완화”되는 장치가 필요합니다.
7-1. 프로브와 종료 훅으로 피해 최소화
OOM은 갑작스러운 SIGKILL이라 우아한 종료가 어렵지만, 그 외 크래시/재시작에 대비해 다음을 정비합니다.
readinessProbe로 트래픽 유입 차단livenessProbe는 너무 공격적으로 잡지 않기(일시적 GC/스파이크에 오탐)terminationGracePeriodSeconds와preStop훅으로 커넥션 드레인
예시:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
terminationGracePeriodSeconds: 30
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
7-2. HPA는 CPU만 보지 말고 메모리도 고려
메모리 스파이크가 트래픽 증가와 연동된다면 HPA에 메모리를 포함하는 것이 효과적입니다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
주의: 메모리 기반 스케일링은 “누수”에는 역효과일 수 있습니다(늘리면 더 오래 버티다 더 크게 터짐). 누수 의심이면 애플리케이션 분석이 우선입니다.
7-3. 리소스 기준선 잡기(측정 기반)
권장 흐름:
- 정상 트래픽과 피크 트래픽에서
working set피크 측정 requests.memory는 “평균+여유”,limits.memory는 “피크+여유”로 설정- 배포 후 1~2일 관찰하고 재조정
7-4. 배포 직후 OOM이면 초기화 로직을 의심
배포 직후만 OOM이면 다음이 흔합니다.
- 캐시 워밍이 한 번에 몰림
- 마이그레이션/인덱싱/대량 프리로드
- 설정 오류로 디버그 로깅/버퍼가 폭증
이 경우에는 limit 상향보다 “초기화 분할, 배치 크기 축소, 지연 로딩”이 근본 해결입니다.
실전 커맨드 모음(복붙용)
아래 순서대로 실행하면 대부분의 사건은 빠르게 분류됩니다.
# 1) 상태/재시작/노드 확인
kubectl -n <namespace> get pod <pod-name> -o wide
# 2) 종료 Reason, Exit Code 확인
kubectl -n <namespace> describe pod <pod-name>
# 3) 직전 컨테이너 로그 확인
kubectl -n <namespace> logs <pod-name> -c <container-name> --previous
# 4) 이벤트 타임라인 확인
kubectl -n <namespace> get events --sort-by='.lastTimestamp'
# 5) 리소스 설정 확인
kubectl -n <namespace> get pod <pod-name> -o jsonpath='{range .spec.containers[*]}{.name}{"\n"}{.resources}{"\n\n"}{end}'
# 6) 현재 사용량(가능한 경우)
kubectl -n <namespace> top pod <pod-name>
# 7) 노드 압박 여부
kubectl describe node <node-name>
마무리: “OOMKilled 확인” 다음이 진짜 실력 포인트
OOMKilled를 확인하는 것 자체는 시작에 불과합니다. 운영에서 중요한 건 다음 두 가지입니다.
- 컨테이너 limit OOM인지, 노드 메모리 압박인지 빠르게 분기하기
- limit을 무작정 올리기보다,
requests/스케일링/초기화 로직/런타임 상한을 함께 손봐서 재발을 막기
애플리케이션이 JVM 기반이라면 힙덤프와 GC 로그로 누수/과할당을 추적하는 단계가 필요할 수 있습니다. 그때는 앞서 언급한 Spring Boot OOM 원인추적과 힙덤프 분석 실전을 참고해 “왜 메모리가 늘었는지”까지 파고들면, 비용을 최소화하면서도 안정성을 크게 올릴 수 있습니다.