CrashLoopBackOff는 “에러”가 아니라 kubelet이 재시작을 포기하기 직전이라는 신호다
CrashLoopBackOff 자체는 원인을 알려주지 않는다. 컨테이너가 시작과 종료를 반복하자 kubelet이 재시작 간격을 10초, 20초, 40초로 두 배씩, 최대 5분까지 늘리며 백오프 상태로 표시한 것뿐이다. 그래서 마지막 로그 한 줄부터 읽으면 대개 헛짚는다. 먼저 볼 것은 컨테이너가 어떤 종료 코드로 죽었는가다.
1단계 — describe로 Last State와 Exit Code부터 읽는다
kubectl describe pod <pod> -n <ns>
Containers 섹션의 Last State: Terminated 아래 Exit Code와 Reason이 진단의 8할이다.
- Exit Code 137 · Reason: OOMKilled — 메모리 한도를 넘겨 SIGKILL. 앱 버그가 아니라
resources.limits.memory가 낮거나 실제 누수다. limit을 올리기 전에 정상 시점 RSS를 먼저 확인한다. - Exit Code 1 / 2 — 앱이 스스로 죽음(예외, 설정 파싱 실패). 이때는
--previous로그가 정답에 가장 가깝다. - Exit Code 0인데 재시작 — 프로세스가 정상 종료했는데
restartPolicy: Always라 다시 뜬다. 배치성 작업을 Deployment로 돌린 전형적 오배치다. - Reason: Error, Exit Code 143 — SIGTERM 후 유예 안에 못 끝나 정리됨. 종료 처리(graceful shutdown)나
terminationGracePeriodSeconds를 본다.
2단계 — 죽은 컨테이너의 로그를 본다 (현재 로그가 아니라)
kubectl logs <pod> -n <ns> --previous
--previous가 없으면 방금 재시작한 빈 컨테이너를 보게 되어 “로그가 없다”고 오판한다. 컨테이너가 여럿이면 -c <container>로 지정한다. initContainer가 원인이면 Pod가 Init:CrashLoopBackOff로 뜨므로 init 컨테이너 로그를 따로 본다.
가장 흔한 착시: 앱은 멀쩡한데 probe가 죽인다
events에 Liveness probe failed가 반복되면 앱 코드가 아니라 probe 타이밍 문제일 때가 많다. 콜드 스타트가 느린 앱(JVM 기동, 시작 시 마이그레이션)은 initialDelaySeconds가 짧으면 준비되기 전에 liveness가 죽여 무한 재시작에 빠진다. 해법은 limit을 늘리는 게 아니라 startupProbe로 기동 구간을 liveness에서 분리하는 것이다.
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 5 # 최대 150초 기동 허용, 그 뒤에만 liveness 시작
readiness와 liveness를 헷갈리지 말 것
readiness 실패는 트래픽만 끊고 재시작시키지 않는다. 그래서 CrashLoopBackOff의 직접 원인은 언제나 liveness 실패 또는 프로세스 종료이지 readiness가 아니다. “재시작이 도는가(liveness/exit)”와 “엔드포인트에서 빠졌는가(readiness)”를 먼저 갈라야 엉뚱한 probe를 만지지 않는다.
구성 누락 계열은 CrashLoop이 아니라 다른 상태로 뜬다
ConfigMap/Secret 참조가 틀리면 보통 CreateContainerConfigError, 이미지를 못 받으면 ImagePullBackOff로 뜬다 — 둘 다 CrashLoopBackOff가 아니다. 상태 문자열을 정확히 먼저 읽으면 이 셋을 섞지 않는다.
복구는 되돌리기 쉬운 순서로
- describe/logs로 읽기 전용 상태 고정 — 종료 코드·probe·이벤트를 먼저 기록한다.
- 원인이 최근 배포면
kubectl rollout undo로 한 리비전만 되돌려 재현 여부를 확인한다. - OOM이면 실측 RSS 기반으로 limit을 조정하고, probe면 startupProbe/initialDelay를 조정한다.
- 전체 재시작·리소스 삭제 같은 비가역 조치는 원인 후보가 하나로 좁혀진 뒤에만 선택한다.
재발 방지 한 가지
사후에는 “왜 오래 못 잡았는가”를 남긴다. 대개 --previous를 안 봤거나, probe 실패를 앱 장애로 오해했거나, 종료 코드를 읽지 않은 것이다. 이 세 줄을 런북 맨 앞에 두면 다음 CrashLoopBackOff는 몇 분 안에 좁혀진다.
빠른 진단 체크리스트
describe의 Last State에서 Exit Code·Reason부터 읽는다- 137·OOMKilled면 메모리 limit/누수, 143이면 SIGTERM 종료 처리를 본다
- Exit 1·2면
kubectl logs --previous로 앱 예외를 본다 - Exit 0인데 재시작이면 restartPolicy·배치 오배치를 의심한다
- events의
Liveness probe failed반복이면 probe 타이밍이다 - 느린 콜드 스타트는
startupProbe로 기동 구간을 분리한다 - readiness(트래픽만)와 liveness(재시작)를 갈라서 본다
- CreateContainerConfigError·ImagePullBackOff와 상태 문자열을 구분한다