- Published on
K8s CrashLoopBackOff·OOMKilled 원인과 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡한데 Pod만 계속 재시작하고, 이벤트에는 CrashLoopBackOff가 찍히며, 가끔은 OOMKilled까지 보이면 운영자는 두 가지를 동시에 의심하게 됩니다. (1) 컨테이너 프로세스가 비정상 종료(Exit Code != 0)하거나 (2) 커널/컨테이너 런타임이 프로세스를 강제 종료(OOM Kill) 했다는 신호입니다. 문제는 이 둘이 서로 얽혀서 “재시작 → 초기화 메모리 급증 → OOMKilled → 재시작” 같은 루프를 만들기 쉽다는 점입니다.
이 글은 원인 분류 → 빠른 진단 명령 → 케이스별 해결 → 재발 방지 체크리스트 순서로 정리합니다.
CrashLoopBackOff와 OOMKilled의 관계 이해
- CrashLoopBackOff: Kubernetes가 컨테이너를 재시작하려고 시도했지만, 짧은 시간 내 반복 실패하여 백오프(backoff) 를 걸고 있는 상태입니다. 근본 원인은 대개
컨테이너 프로세스 종료입니다. - OOMKilled: 리눅스 OOM Killer 또는 cgroup 메모리 제한에 의해 프로세스가 강제 종료된 상태입니다. Kubernetes에서는
Last State: Terminated에Reason: OOMKilled로 표시됩니다.
즉, OOMKilled는 CrashLoopBackOff의 “대표적인 원인 중 하나” 입니다. 반대로, CrashLoopBackOff가 OOMKilled가 아닌 경우도 매우 많습니다(예: 잘못된 환경변수, Secret 누락, 프로브 실패로 인한 kill 등).
1분 진단: kubectl로 원인 좁히기
아래 순서대로 보면 대부분의 케이스는 5분 내에 윤곽이 나옵니다.
1) Pod 이벤트와 종료 사유 확인
kubectl -n <ns> describe pod <pod-name>
여기서 핵심 포인트:
State: Waiting/Reason: CrashLoopBackOffLast State: Terminated/Reason: OOMKilled또는ErrorExit Code: 137(OOMKilled 가능성 큼),Exit Code: 1(앱 에러),Exit Code: 143(SIGTERM 정상 종료 흐름일 수도)Events섹션의Killing/Back-off restarting failed container/Readiness probe failed등
2) 직전 크래시 로그 확인
kubectl -n <ns> logs <pod-name> -c <container-name> --previous
--previous는 “바로 직전에 죽은 컨테이너” 로그를 봅니다.- 애플리케이션이 시작 직후 죽는다면, 이 로그가 거의 유일한 단서인 경우가 많습니다.
3) 리소스 사용량(있다면) 확인
kubectl -n <ns> top pod <pod-name>
top이 0%로만 나오거나 동작하지 않으면 Metrics Server/metrics API 문제일 수 있습니다. 이 경우는 별도 점검이 필요합니다: EKS에서 kubectl top이 0%일 때 Metrics API 점검
OOMKilled: 가장 흔한 원인과 해결
원인 A) memory limit이 너무 작다(초기화/캐시/워크로드 피크)
가장 흔한 케이스입니다. 특히 다음 패턴에서 잘 터집니다.
- JVM/Node/Python 앱이 시작 시 JIT/캐시/모듈 로딩으로 메모리 피크
- 대량 트래픽 시 버퍼/큐/커넥션 증가
- 로그/메트릭 에이전트가 함께 떠서 합산 메모리 증가
해결 1) requests/limits 재설계
requests.memory: 스케줄링 기준(노드에 자리 잡기)limits.memory: cgroup 상한(넘으면 OOMKilled)
예시(너무 타이트한 limit을 완화):
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
운영 팁:
- limit을 올리기 전에 “왜 그만큼 필요한지”를 확인하세요(메모리 누수 vs 정상 피크).
- HPA를 쓰더라도, 단일 Pod가 감당 못 하는 피크가 있으면 scale-out 전에 죽을 수 있습니다.
해결 2) 언어/런타임별 메모리 상한 설정
- JVM: 컨테이너 환경에서 힙이 limit에 비해 과도하게 잡히면 OOM이 납니다.
JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75 -XX:InitialRAMPercentage=50"
- Node.js: 기본 힙 상한이 컨테이너 limit과 불일치할 수 있습니다.
NODE_OPTIONS="--max-old-space-size=768"
- Python (gunicorn/uvicorn): 워커 수가 많으면 메모리가 선형 증가합니다. 워커 수를 CPU 기반으로만 잡지 말고 메모리도 고려하세요.
원인 B) 메모리 누수(특히 장시간 실행 후)
증상:
- 처음엔 정상 → 시간이 지나면서 RSS가 계속 증가 → limit 도달 시 OOMKilled
대응:
- 애플리케이션 레벨에서 heap dump/pprof 등으로 누수 지점 추적
- 단기 완화로는 rolling restart 또는
max-requests같은 워커 재생성 옵션
예: gunicorn의 워커 재시작(누수 완화)
gunicorn app:app --max-requests 2000 --max-requests-jitter 200
원인 C) 노드 레벨 메모리 압박(Eviction)
Pod가 OOMKilled가 아니라 Evicted로 죽기도 합니다. describe pod의 이벤트에 The node was low on resource: memory 등이 보이면 노드 압박입니다.
대응:
- 노드 타입 상향/노드 수 증가(Cluster Autoscaler)
- 과도한 DaemonSet(로그/보안 에이전트) 메모리 점검
requests를 현실적으로 설정해 과밀 스케줄링 방지
CrashLoopBackOff: OOM이 아닌 대표 원인들
원인 D) liveness/readiness/startup probe 설정이 공격적
앱이 아직 준비되지 않았는데 liveness가 먼저 실패하면 kubelet이 컨테이너를 계속 kill합니다. 특히 초기화가 느린 앱에서 흔합니다.
권장 패턴:
- 느리게 뜨는 앱:
startupProbe로 부팅 구간 보호 livenessProbe는 “죽었을 때만 실패”하도록 보수적으로readinessProbe는 트래픽 차단 용도
예시:
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 2
livenessProbe:
httpGet:
path: /live
port: 8080
initialDelaySeconds: 30
timeoutSeconds: 2
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
timeoutSeconds: 2
periodSeconds: 5
failureThreshold: 2
체크 포인트:
- readiness 실패는 재시작을 유발하지 않습니다.
- liveness 실패는 재시작을 유발합니다.
원인 E) 환경변수/Secret/ConfigMap 누락 또는 권한 문제
컨테이너는 뜨지만 앱이 시작하면서 설정을 못 읽고 종료 → CrashLoopBackOff.
진단:
kubectl describe pod에서MountVolume.SetUp failed/secret not found- 앱 로그에
AccessDenied,NoSuchKey,permission denied
EKS에서 AWS 리소스 권한 문제(예: S3 403)도 흔한 크래시 원인입니다. 관련해서는 이 글의 체크리스트가 도움이 됩니다: EKS Pod에서 S3 403 AccessDenied 원인 10가지
원인 F) 종료 처리 미흡(Graceful shutdown 실패)로 재시작 루프 확대
Pod가 재시작될 때 SIGTERM을 받는데, 애플리케이션이 종료 훅에서 오래 걸리거나 데드락이 걸리면 다음 문제가 생깁니다.
- 새 Pod가 뜨기 전에 구 Pod가 끝나지 않아 리소스가 겹침
- 연결 정리가 안 되어 외부 의존성(DB 등)에 부담
대응:
terminationGracePeriodSeconds를 현실적으로- 애플리케이션에서 SIGTERM 처리(서버 close, 큐 flush, 워커 stop)
예시:
terminationGracePeriodSeconds: 60
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
preStop의 sleep은 로드밸런서/엔드포인트 갱신 시간을 벌어주지만, 근본적으로는 앱이 정상 종료 루틴을 가져야 합니다.
실전 디버깅 루틴(재현 가능한 체크리스트)
1) “OOMKilled인지”부터 확정
kubectl -n <ns> get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
OOMKilled면 리소스/런타임 튜닝으로 바로 들어갑니다.Error면 로그/설정/프로브/의존성 문제로 접근합니다.
2) Exit Code로 힌트 얻기
137: SIGKILL(대개 OOM)143: SIGTERM(정상 종료 흐름일 수도)1: 앱 내부 에러
kubectl -n <ns> get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'
3) 노드/이벤트에서 eviction, OOM pressure 확인
kubectl -n <ns> get events --sort-by=.lastTimestamp | tail -n 50
kubectl describe node <node-name>
재발 방지: 운영 관점의 설계 포인트
리소스는 “관측 → 가설 → 조정”으로 반복
- 메모리 limit은 안전장치지만, 너무 낮으면 안정성을 오히려 해칩니다.
- 반대로 무작정 올리면 노드 밀도가 떨어지고 비용이 증가합니다.
권장:
- 관측(메트릭/로그)으로 피크 패턴 파악
- 초기화 피크 vs 누수 vs 트래픽 피크 구분
- limit 조정 + 런타임 상한 설정 + 프로브 완화
장애를 “연쇄”로 보라
CrashLoopBackOff는 종종 다른 장애의 결과입니다.
- DB 커넥션 고갈 → 앱이 시작 시 연결 실패로 종료 → CrashLoopBackOff
- 외부 API 5xx → 초기화 단계에서 실패 → 재시작 루프
외부 의존성 실패에 대해선 재시도/폴백/서킷브레이커가 크래시를 막는 실전 대안이 됩니다. 패턴 정리는 다음 글이 참고됩니다: OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커
결론: 가장 빠른 해결 순서
describe pod로 Last State / Reason / Exit Code 확인logs --previous로 직전 크래시 로그 확보OOMKilled면: limit 상향 + 런타임 상한(JVM/Node) + 워커 수/캐시 점검- OOM이 아니면: probe(특히 liveness/startup)·설정 누락·권한·의존성 장애를 우선 확인
- 마지막으로 노드 압박/eviction까지 확장
원하시면 사용 중인 런타임(JVM/Node/Python/Go), 현재 resources, probe 설정, 그리고 describe pod 출력 일부를 주시면 “딱 필요한 수준”의 수정안(YAML 패치 형태)으로 구체화해 드릴게요.