- Published on
K8s CrashLoopBackOff - Readiness·Liveness 5분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 갑자기 불안정해졌는데 kubectl get pods에 CrashLoopBackOff가 뜨면, 대부분의 팀은 곧바로 “애플리케이션이 크래시 났다”라고 단정합니다. 하지만 실제 현장에서는 프로세스는 살아있는데 Liveness probe가 계속 죽이는 경우, 혹은 초기 기동이 느린데 Startup probe가 없어 Liveness가 너무 빨리 개입하는 경우가 꽤 흔합니다.
이 글은 CrashLoopBackOff를 5분 안에 분류하고(진짜 크래시 vs 프로브/설정 문제), Readiness/Liveness/Startup을 어떤 순서로 확인해야 하는지, 그리고 어떤 설정이 자주 사고를 만드는지에 초점을 맞춥니다.
> 참고: 증상이 “이미지조차 못 받음”이라면 CrashLoopBackOff가 아니라 ImagePullBackOff로 시작하는 경우가 많습니다. EKS에서 403이 섞이면 EKS ImagePullBackOff 403? ECR VPC 엔드포인트 정책 점검도 함께 확인하세요.
CrashLoopBackOff를 5분 안에 분류하는 흐름
핵심은 “왜 재시작되는지”를 이벤트와 종료 코드로 먼저 잡는 것입니다. 아래 순서대로 보면 대부분 5분 내에 방향이 잡힙니다.
1) 재시작 원인 1차 확인: describe + lastState
kubectl get pod -n <ns> <pod> -o wide
kubectl describe pod -n <ns> <pod>
describe의 Events 섹션에서 다음 패턴을 찾습니다.
Liveness probe failed→ 프로브가 컨테이너를 죽이고 있을 가능성 큼Readiness probe failed만 반복 → 트래픽은 안 받지만 크래시는 아닐 수 있음(단, 재시작은 다른 이유)Back-off restarting failed container+Exit Code: 1/137/139/...→ 앱 종료/시그널/메모리 문제 등
종료 코드와 마지막 상태도 바로 봅니다.
kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
kubectl get pod -n <ns> <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
자주 보는 종료 코드 힌트:
137→ OOMKill 또는 SIGKILL(메모리/리소스)139→ Segfault1→ 일반 오류(설정/환경변수/의존성 실패)
2) 로그는 “이전 컨테이너”부터 본다
CrashLoopBackOff에서는 현재 컨테이너가 이미 죽었을 수 있으니 --previous가 핵심입니다.
kubectl logs -n <ns> <pod> -c <container> --previous --tail=200
여기서 앱이 스스로 종료하는지, 아니면 프로브가 실패하기 직전까지 정상 동작하는지 감을 잡습니다.
3) 프로브가 원인인지 즉시 판별하는 질문 3개
- Liveness 실패 이벤트가 있는가?
- 초기 기동 시간이 긴 앱인데 Startup probe가 없는가?
- Readiness가 실패했을 때도 Liveness가 같이 실패하는가? (같은 엔드포인트/같은 조건을 공유하는 경우가 많음)
이 3개 중 1~2개가 걸리면, “앱이 죽어서 CrashLoopBackOff”가 아니라 “살아있는데 쿠버네티스가 죽이는 CrashLoopBackOff”일 확률이 높습니다.
Readiness vs Liveness vs Startup: 역할을 섞으면 사고 난다
많이들 알고 있지만, 실제 YAML에서 섞이는 순간 문제가 터집니다.
Readiness probe: ‘트래픽을 받을 준비’만 판단
- 실패해도 컨테이너를 재시작하지 않습니다.
- 실패하면 Service 엔드포인트에서 제외되어 트래픽이 안 들어옵니다.
올바른 Readiness의 질문:
- DB 마이그레이션/캐시 워밍업이 끝났나?
- 다운스트림 의존성(예: Redis, Kafka)이 “필수”로 준비됐나?
Liveness probe: ‘회복 불가능한 상태’면 재시작
- 실패하면 kubelet이 컨테이너를 kill 후 재시작합니다.
- 너무 공격적으로 설정하면 정상인 앱도 죽입니다.
올바른 Liveness의 질문:
- 스레드 데드락/이벤트루프 스톨처럼 “스스로 회복 못 하는” 상태인가?
- 단순히 DB가 잠깐 느린 것까지 재시작으로 해결되는가? (대부분 NO)
Startup probe: ‘초기 기동이 끝날 때까지 Liveness를 유예’
- 초기 기동이 느린 앱(스프링, 대형 Node 서버, 모델 로딩 등)은 Startup이 사실상 필수입니다.
- Startup이 성공한 뒤에만 Liveness가 의미 있게 동작하도록 만듭니다.
가장 흔한 CrashLoopBackOff 패턴 6가지와 해결
1) “Readiness 엔드포인트”를 Liveness에 그대로 복붙
Readiness는 외부 의존성(DB 등)을 포함하는 경우가 많습니다. 이를 Liveness에 복붙하면 DB가 잠깐 느려져도 컨테이너를 재시작해버립니다.
증상
- 이벤트:
Liveness probe failed: HTTP probe failed with statuscode: 500 - 앱 로그: DB 연결 실패/타임아웃이 간헐적으로 보임
해결
- Liveness는 프로세스 생존(이벤트루프 응답, 내부 큐 상태 등) 위주로 단순화
- Readiness에만 의존성 체크를 넣기
예시(권장 구조):
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
2) 초기 기동이 긴데 Startup probe가 없음
스프링 부트, 대형 번들 로딩, 마이그레이션, 모델 로딩 등으로 4090초 걸리는 앱에서 흔합니다. Liveness가 1020초부터 실패를 누적하면, 앱이 뜨기도 전에 kill → CrashLoopBackOff.
해결
startupProbe:
httpGet:
path: /healthz/startup
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 30 # 5초 * 30 = 150초까지 기동 유예
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
포인트는 Startup이 있는 동안 Liveness는 실패해도 재시작하지 않는다는 점입니다.
3) timeoutSeconds가 너무 짧고, periodSeconds가 너무 촘촘함
특히 JVM GC, Node 이벤트루프 블로킹, CPU throttling이 있는 환경에서 timeoutSeconds: 1은 생각보다 가혹합니다.
해결 가이드(경험칙)
- HTTP 기반이면
timeoutSeconds: 2~5부터 시작 periodSeconds를 5초 이하로 줄이기 전에 CPU/GC/스파이크를 관찰failureThreshold를 올려 “일시적 지연”을 흡수
4) OOMKill(Exit 137)인데 프로브로 착각
메모리 제한이 타이트하면, 앱이 정상 기동하다가 특정 요청/캐시 워밍업에서 메모리가 튀며 OOMKill → 재시작 → CrashLoopBackOff.
확인:
kubectl describe pod -n <ns> <pod> | sed -n '/Last State:/,/Events:/p'
Reason: OOMKilled가 보이면 프로브보다 requests/limits, 메모리 사용 패턴이 우선입니다.
해결:
resources.limits.memory상향 또는 메모리 누수/캐시 정책 점검- JVM이면
-Xmx를 limit에 맞추기(여유 포함)
5) exec probe에서 셸/유틸리티 부재로 실패
exec 프로브에서 curl을 호출했는데 이미지에 curl이 없거나, sh -c가 없는 distroless 이미지면 계속 실패합니다.
나쁜 예:
livenessProbe:
exec:
command: ["sh", "-c", "curl -f http://localhost:8080/health || exit 1"]
해결:
- 가능하면
httpGet사용 - 꼭 exec가 필요하면 이미지에 필요한 바이너리 포함 또는 앱 내부 헬스체크 바이너리 제공
6) 애플리케이션이 0.0.0.0이 아니라 127.0.0.1 외 주소에 바인딩
프로브는 기본적으로 Pod IP로 컨테이너에 접근합니다. 앱이 특정 인터페이스에만 바인딩하면 Connection refused가 납니다.
확인:
- 로그에 “Listening on 127.0.0.1:8080” 같은 문구
해결:
0.0.0.0바인딩으로 변경- 또는
httpGet.host를 조정(권장되진 않음)
kubectl로 프로브/네트워크를 빠르게 재현하기
프로브가 실패한다면 “Pod 내부에서 실제로 요청이 되는지”를 재현해야 합니다.
1) 같은 네임스페이스에서 임시 디버그 파드 실행
kubectl run -n <ns> netshoot --rm -it --image=nicolaka/netshoot -- bash
파드 안에서:
curl -sv http://<pod-ip>:8080/healthz/ready
curl -sv http://<pod-ip>:8080/healthz/live
2) 대상 파드에 ephemeral container로 붙기(권한 필요)
kubectl debug -n <ns> -it <pod> --image=nicolaka/netshoot --target=<container>
이 방식은 “컨테이너 내부 네임스페이스”에서 확인할 수 있어, 로컬호스트 바인딩/iptables 이슈를 더 잘 드러냅니다.
안전한 프로브 설계 템플릿(실무 기준)
아래는 많은 팀에서 무난하게 시작할 수 있는 기본형입니다.
- Startup: 기동 완료까지 충분히 유예
- Liveness: 외부 의존성 제외, 빠르고 단순
- Readiness: 트래픽 수용 가능 여부(의존성 포함 가능)
startupProbe:
httpGet:
path: /healthz/startup
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 24 # 2분 유예
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
추가로, 롤링 업데이트 중 안정성을 높이려면 terminationGracePeriodSeconds와 프리-스톱 훅, 그리고 애플리케이션의 graceful shutdown도 같이 맞춰야 합니다.
(보너스) Ingress/ALB 타임아웃을 Crash로 오해하는 경우
가끔 “외부에서 408/504가 난다”를 CrashLoopBackOff로 연결해 조사하다가 시간을 버립니다. 파드는 멀쩡히 Running인데, Ingress/ALB 타임아웃/헬스체크 설정이 원인인 케이스도 많습니다. EKS에서 ALB 408이 반복된다면 EKS에서 ALB Ingress 408 Request Timeout 해결 가이드를 함께 보세요.
5분 체크리스트(요약)
kubectl describe pod이벤트에서 Liveness 실패가 있는지 먼저 확인kubectl logs --previous로 “앱이 스스로 죽는지/죽임당하는지” 구분- 초기 기동이 길면 Startup probe부터 도입
- Liveness는 외부 의존성 체크 금지(대부분 Readiness로)
timeoutSeconds=1같은 과격한 값은 CPU/GC 스파이크에서 재시작 루프를 만든다- Exit 137이면 프로브보다 메모리/리소스가 우선
CrashLoopBackOff는 현상일 뿐이고, 원인은 크게 두 갈래(앱 크래시 vs 쿠버네티스가 죽임)로 빨리 나눌수록 해결이 빨라집니다. 위 흐름대로 이벤트/종료코드/프로브 설계를 점검하면, “왜 계속 재시작되는지”는 대부분 짧은 시간 안에 결론이 납니다.