- Published on
EKS CrashLoopBackOff - OOMKilled·Exit 137 원인과 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡한데 EKS Pod가 CrashLoopBackOff로 계속 재시작되고, kubectl describe pod에는 OOMKilled가 찍히거나 Exit Code: 137만 남는 경우가 많습니다. 이때 흔히 “메모리 늘리면 되겠지”로 끝내지만, 실제 현장에서는 (1) 컨테이너 메모리 제한 때문에 커널 OOM이 난 것인지, (2) 노드 메모리 압박으로 축출(eviction)된 것인지, **(3) 앱이 SIGKILL을 맞을 만한 다른 이유(프로브, preStop, 런타임, sidecar)**가 있는지부터 분리해야 재발을 막을 수 있습니다.
이 글은 EKS/Kubernetes에서 CrashLoopBackOff + OOMKilled/137을 만났을 때 증거를 모으는 순서와 원인별 해결책을 실전 관점으로 정리합니다.
> 참고: CrashLoopBackOff가 사실은 liveness probe 오탐인 경우도 매우 흔합니다. 그 케이스는 별도로 정리한 글(K8s CrashLoopBackOff - liveness probe 오탐 해결)을 함께 보시면 진단 시간이 크게 줄어듭니다.
CrashLoopBackOff, OOMKilled, Exit 137의 관계
CrashLoopBackOff란?
CrashLoopBackOff는 “컨테이너가 반복적으로 죽고(restart) Kubernetes가 재시작 간격을 점점 늘리는(back-off) 상태”입니다. 즉 원인은 CrashLoopBackOff가 아니라, 반복 종료를 유발한 사건입니다.
OOMKilled란?
OOMKilled는 리눅스 커널 OOM Killer가 프로세스를 죽였다는 뜻입니다. Kubernetes 관점에서는 보통:
- 컨테이너가 **자신의 cgroup 메모리 제한(
resources.limits.memory)**을 초과해서 SIGKILL - 또는 노드가 메모리 압박을 받아 eviction이 발생(이 경우는
OOMKilled가 아닐 수도 있음)
Exit Code 137이 의미하는 것
137 = 128 + 9 이고, 여기서 9는 SIGKILL입니다. 즉 강제 종료(kill -9) 를 당했다는 뜻입니다.
중요 포인트:
Exit 137은 OOMKilled일 수도 있고 아닐 수도 있습니다.- 예: 노드가 압박으로 컨테이너 런타임이 강제 종료, 또는 사람이
kubectl delete pod로 종료(Grace 기간 후 SIGKILL), 또는 프로브/오케스트레이션이 강제 종료 등.
따라서 Exit 137을 봤다면 OOMKilled 여부를 먼저 확인해야 합니다.
1단계: “무엇이 죽였는지” 빠르게 확인하는 커맨드
Pod 이벤트/상태에서 1차 판별
kubectl get pod -n <ns> <pod> -o wide
kubectl describe pod -n <ns> <pod>
describe에서 특히 봐야 할 부분:
Last State: Terminated의Reason이OOMKilled인지Exit Code: 137/Exit Code: 1등Events에Killing,Back-off restarting failed container외에EvictedThe node was low on resource: memoryContainer killed by kubelet같은 메시지가 있는지
컨테이너별 종료 사유를 JSONPath로 바로 뽑기
멀티 컨테이너(사이드카 포함)면 어떤 컨테이너가 죽는지부터 헷갈립니다.
kubectl get pod -n <ns> <pod> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'
직전 로그(이전 컨테이너) 확인
재시작이 반복되면 현재 로그만 보면 원인이 사라져 있습니다.
kubectl logs -n <ns> <pod> -c <container> --previous --tail=200
- OOM이면 보통 앱 로그가 “갑자기 끊겨” 있습니다.
- 반대로 앱이 스스로 죽는다면(예: config error) 종료 직전 에러가 남는 경우가 많습니다.
2단계: OOMKilled인지, 노드 메모리 압박(eviction)인지 구분
케이스 A: Reason: OOMKilled가 명확한 경우
이 경우는 거의 항상 컨테이너 메모리 limit 초과입니다.
확인할 것:
resources.limits.memory가 너무 낮지 않은가?- JVM/Node/Python 등 런타임이 limit을 인지하고 힙을 제한하고 있는가?
- 순간 피크(배치, 캐시 워밍, 대형 응답 생성)로 limit을 넘는가?
케이스 B: Exit 137인데 OOMKilled는 아닌 경우
이때는 다음을 의심합니다.
- 노드 메모리 압박으로 eviction
- 이벤트에
Evicted또는low on resource: memory가 찍힙니다. - Pod 자체는 limit을 안 넘었는데도 노드 전체가 부족해서 밀려날 수 있습니다.
- 종료(graceful) 실패 후 SIGKILL
terminationGracePeriodSeconds동안 종료가 안 되면 kubelet이 SIGKILL을 보냅니다.- 이 경우도 137이 나올 수 있습니다.
- liveness probe 오탐으로 강제 재시작
- 앱은 살아있지만 probe가 실패해서 kubelet이 컨테이너를 죽입니다.
- 자세한 패턴과 해결은 내부 글(K8s CrashLoopBackOff - liveness probe 오탐 해결) 참고.
3단계: 리소스 설정을 “증거 기반”으로 조정하기
requests/limits가 왜 중요한가
requests: 스케줄링 보장(노드에 자리 잡기)limits: 상한선(cgroup 제한). 메모리 limit을 넘으면 OOMKilled
특히 EKS에서 다음 조합이 사고를 만듭니다.
requests는 낮게 잡아 쉽게 스케줄링limits는 타이트하게 잡음- 실제 트래픽/피크에서 메모리 스파이크 → OOMKilled → CrashLoopBackOff
현재 사용량(근사치) 확인
metrics-server가 있다면:
kubectl top pod -n <ns>
kubectl top pod -n <ns> --containers
kubectl top node
주의: kubectl top은 순간값이라 OOM 직전 피크를 놓치기 쉽습니다. 가능하면 Prometheus/Grafana에서 container_memory_working_set_bytes 같은 시계열로 피크를 확인하세요.
(예시) 안전한 리소스 템플릿
아래는 “일단 죽지 않게” 잡는 예시이고, 서비스 특성에 맞게 조정해야 합니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
image: your-image:tag
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
실무 팁:
- OOMKilled가 났다면 limits.memory만 올리는 것은 임시방편입니다.
requests.memory도 함께 올려야 노드에 “제대로 자리”를 잡고, 노드 압박/eviction 가능성을 줄입니다.
4단계: 런타임별 OOM 패턴과 해결
JVM (Spring Boot 등): limit을 몰라서 힙이 과도해지는 문제
컨테이너 환경에서 JVM은 버전에 따라 cgroup을 잘못 인식하거나, 기본 힙이 너무 커질 수 있습니다.
권장 접근:
-XX:MaxRAMPercentage로 힙 상한을 명시- 메타스페이스/직접 메모리(Direct), 스레드 스택 등도 고려
예:
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50 -XX:+ExitOnOutOfMemoryError"
Node.js: old space 기본값/버전별 차이
Node는 힙 제한이 컨테이너 limit과 다르게 동작할 수 있습니다.
예:
env:
- name: NODE_OPTIONS
value: "--max-old-space-size=768"
Python: 캐시/버퍼/대형 객체 누수
- pandas/pyarrow 등은 순간 메모리 피크가 큼
- gunicorn worker 수가 많으면 선형으로 메모리 증가
점검:
- worker 수/threads 조정
- 요청당 큰 객체 생성 여부
- 이미지/압축/파싱 로직의 peak memory
5단계: 노드 메모리 압박(Eviction)일 때의 해결
Pod limit을 올렸는데도 계속 죽는다면 “노드가 작아서” 생기는 문제일 수 있습니다.
확인 포인트
kubectl describe node <node-name>
Conditions에MemoryPressure=TrueAllocatable대비Requested가 과도
해결 옵션
- 노드 인스턴스 타입 업그레이드(메모리 큰 타입)
- 노드 수 확장(Cluster Autoscaler/Karpenter)
- 과도한 DaemonSet(로그/모니터링 에이전트) 메모리 사용량 점검
- QoS 개선:
requests를 적절히 설정해BestEffort를 피하고, 중요한 Pod가 먼저 쫓겨나지 않게 설계
6단계: 종료(graceful) 실패로 137이 나는 케이스
앱이 SIGTERM을 받았는데 종료가 오래 걸리거나 멈추면, kubelet이 grace 기간 후 SIGKILL(137)을 보냅니다.
점검 체크리스트
terminationGracePeriodSeconds가 너무 짧지 않은가?- preStop hook이 너무 오래 걸리지 않는가?
- SIGTERM 핸들링이 제대로 되어 있는가?
예시:
spec:
terminationGracePeriodSeconds: 60
containers:
- name: api
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
preStop는 트래픽 드레이닝에 유용하지만, 과하면 오히려 종료 지연으로 137을 유발합니다.
7단계: 프로브(liveness/readiness)로 인한 재시작과 OOM의 구분
OOM과 프로브 실패는 증상이 비슷합니다(재시작 반복). 하지만 해결책은 완전히 다릅니다.
- OOM: 메모리 상한/피크/누수/런타임 옵션
- liveness 오탐: 초기 지연, timeout, endpoint 비용, GC/Stop-the-world 타이밍, 과도한 실패 임계치
프로브가 의심되면 아래 글의 “오탐 패턴(초기 부하, GC, DB 커넥션, cold start)” 섹션이 특히 도움이 됩니다.
8단계: 재현 가능한 디버깅 루틴(현장용)
아래 루틴대로 하면 대부분의 CrashLoopBackOff + 137을 10~30분 안에 범주화할 수 있습니다.
- 종료 사유 확정
kubectl describe pod -n <ns> <pod>
kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated}'
- 직전 로그 확인
kubectl logs -n <ns> <pod> --previous --tail=200
- 이벤트에서 eviction/pressure 확인
kubectl get events -n <ns> --sort-by=.lastTimestamp | tail -n 50
- 노드 상태 확인(특히 MemoryPressure)
kubectl describe node <node>
kubectl top node
- 리소스/프로브/종료 설정 점검
resources.requests/limitslivenessProbe/readinessProbe/startupProbeterminationGracePeriodSeconds,preStop
9단계: “해결”을 넘어 재발 방지(권장)
- HPA/VPA: 트래픽 변동이 크면 HPA로 수평 확장, 메모리 기반이면 VPA도 검토
- 메모리 피크 관측: OOM은 평균이 아니라 피크가 죽입니다. 피크 지표를 대시보드에 고정
- 부하 테스트 시나리오에 워밍/캐시/대형 요청 포함: cold start나 대형 payload에서만 터지는 OOM이 많습니다.
마무리: 137을 보면 “OOM인가?”부터, OOM이면 “왜 limit을 넘었나?”까지
정리하면,
CrashLoopBackOff는 결과이고, 원인은 따로 있습니다.Exit 137은 SIGKILL이며, OOMKilled일 수도/아닐 수도 있습니다.Reason: OOMKilled가 보이면 컨테이너 메모리 limit 초과 가능성이 매우 높고, 런타임별 힙/캐시/worker 설정까지 함께 조정해야 합니다.- OOMKilled가 아니라면 eviction, 종료 지연, 프로브 오탐을 우선순위로 의심하세요.
프로브 오탐으로 재시작이 반복되는 케이스는 진단 접근이 달라서, 아래 글을 같이 보면 훨씬 빨리 결론에 도달합니다.