- Published on
K8s CrashLoopBackOff - readinessProbe 실패 7원인
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CrashLoopBackOff는 컨테이너가 비정상 종료되어 kubelet이 재시작을 반복하는 상태입니다. 여기에 readinessProbe 실패가 함께 보이면 많은 팀이 "레디니스가 실패해서 재시작한다"고 오해합니다. 실제로는 재시작의 직접 원인은 프로세스 종료이고, readinessProbe는 트래픽 라우팅(Ready 여부) 에 영향을 줍니다. 다만 readinessProbe 실패가 "애플리케이션이 정상 상태로 올라오지 못했다"는 강력한 신호이기 때문에, CrashLoopBackOff 원인 분석의 출발점으로 매우 유용합니다.
이 글은 readinessProbe 실패를 동반한 CrashLoopBackOff에서 자주 반복되는 7가지 원인을, 점검 커맨드와 설정 예시 중심으로 정리합니다.
먼저 확인할 것: 이벤트와 종료 코드부터
아래 3가지만 먼저 보면 방향이 빠르게 잡힙니다.
- Pod 이벤트: 프로브 실패 메시지, 이미지 풀, 볼륨 마운트, OOMKilled 등
- 컨테이너 종료 코드:
1,137,143등 - 직전 로그: 프로세스가 왜 죽는지
kubectl describe pod -n $NS $POD
kubectl get events -n $NS --sort-by=.lastTimestamp | tail -n 50
# 현재 컨테이너(재시작 후) 로그
kubectl logs -n $NS $POD -c $CONTAINER
# 직전(죽기 전) 컨테이너 로그
kubectl logs -n $NS $POD -c $CONTAINER --previous
# 종료 코드 확인(필드가 여러 개면 컨테이너 이름을 맞춰 확인)
kubectl get pod -n $NS $POD -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'
이제 readinessProbe 실패와 함께 잘 엮이는 7가지 원인을 보겠습니다.
1) 프로브 경로/포트가 실제 리스닝과 불일치
가장 흔합니다. 애플리케이션은 8080에 뜨는데 프로브는 80을 찌르거나, /health가 아닌 /actuator/health를 찌르는 식입니다. 또는 컨테이너는 127.0.0.1에만 바인딩되어 Pod IP에서 접근이 안 되는 경우도 있습니다.
증상
- 이벤트에
Readiness probe failed: Get ... connection refused또는404 - 컨테이너는 살아 있지만 Ready가 되지 않음
- 트래픽이 안 들어오고, 상위 컴포넌트가 이를 장애로 간주해 재배포/롤백
점검
# Pod 내부에서 실제로 응답하는지 확인
kubectl exec -n $NS -it $POD -- sh -lc 'wget -qO- http://127.0.0.1:8080/health || true'
# 컨테이너가 어떤 포트에 리스닝하는지
kubectl exec -n $NS -it $POD -- sh -lc 'netstat -lntp 2>/dev/null || ss -lntp'
개선 예시
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 6
2) 애플리케이션 워밍업이 긴데 initialDelaySeconds가 너무 짧음
Spring Boot, Node.js, Python, JVM 기반 서비스는 시작 시 마이그레이션, 캐시 로딩, 클래스 로딩 등으로 워밍업이 길어질 수 있습니다. 이때 readinessProbe가 너무 일찍 시작되면 실패가 누적되고, 운영 자동화(오토스케일/롤링 업데이트/서비스 메시)가 이를 장애로 확대시킬 수 있습니다.
증상
- 일정 시간이 지나면 정상인데, 시작 직후만 연속 실패
- 이벤트에
context deadline exceeded또는timeout
권장 패턴: startupProbe 사용
startupProbe는 "부팅 중" 구간을 별도로 보호해 주며, 통과한 이후에만 readiness/liveness가 의미 있게 동작합니다.
startupProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 60 # 60 * periodSeconds 동안 부팅 허용
periodSeconds: 2
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
timeoutSeconds: 2
3) readinessProbe가 외부 의존성(DB, Redis, 외부 API)에 과도하게 결합
readinessProbe는 "이 Pod가 트래픽을 받아도 되는가"를 판단하는 용도지만, 이를 DB 연결/외부 API 호출까지 포함해 "모든 의존성이 완벽"해야만 Ready가 되도록 만들면 작은 장애가 곧바로 대규모 언레디로 번집니다.
특히 DB 커넥션 풀이 고갈되면 readiness 체크 자체가 DB 커넥션을 못 얻어서 실패하고, 그 결과 트래픽이 더 튀는 악순환이 발생합니다. Spring 계열이라면 HikariCP 고갈을 함께 점검하세요: Spring Boot HikariCP 커넥션 고갈 원인과 해결 가이드
개선 가이드
- readiness는 "프로세스가 요청을 처리할 준비" 중심(스레드풀, 큐, 내부 상태)
- 외부 의존성은 별도 지표/알람으로 관리하거나, 최소한 타임아웃을 매우 짧게
- 가능한 경우
/readyz는 경량,/healthz는 종합으로 분리
예시: 타임아웃이 있는 exec 프로브
readinessProbe:
exec:
command:
- sh
- -lc
- 'wget -qO- --timeout=1 http://127.0.0.1:8080/readyz | grep -q "ok"'
periodSeconds: 5
timeoutSeconds: 2
4) DNS 장애 또는 네임리졸브 지연으로 내부 호출이 막힘
애플리케이션이 시작 시 설정 서버, DB, 메시지 브로커 등을 DNS로 찾는다면, CoreDNS 장애나 노드 레벨 네임리졸브 이슈가 readiness 실패로 나타날 수 있습니다. 이 경우 readinessProbe 자체는 로컬 HTTP인데도, 애플리케이션이 의존성 초기화에서 막혀 서버가 뜨지 못해 connection refused가 됩니다.
EKS 환경에서 DNS 타임아웃이 의심되면 아래 글의 체크리스트가 그대로 적용됩니다: EKS CoreDNS 장애? DNS 타임아웃 8단계
점검
# Pod 내부 DNS 확인
kubectl exec -n $NS -it $POD -- sh -lc 'cat /etc/resolv.conf; nslookup kubernetes.default.svc.cluster.local || true'
# 특정 의존성 호스트 확인
kubectl exec -n $NS -it $POD -- sh -lc 'nslookup mydb.my-namespace.svc.cluster.local || true'
개선
- CoreDNS 리소스/레플리카 점검
- 애플리케이션의 의존성 초기화에 타임아웃/리트라이/백오프 적용
- readiness가 외부 의존성에 매달리지 않도록 분리
5) 리소스 부족(OOMKilled, CPU 스로틀링)으로 부팅 중 종료
CrashLoopBackOff의 고전 원인입니다. 메모리가 부족하면 exitCode가 137로 남고, kubelet 이벤트에 OOMKilled가 찍힙니다. CPU가 너무 낮으면 부팅이 느려져 readiness 타임아웃이 누적될 수 있습니다.
점검
kubectl describe pod -n $NS $POD | sed -n '1,200p'
# 메트릭 서버가 있다면
kubectl top pod -n $NS $POD
kubectl top node
개선 예시
requests는 "평균"이 아니라 "부팅 피크"도 고려- JVM이라면 힙 상한이 컨테이너 메모리를 넘지 않게 조정
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
6) 프로브 타임아웃/주기 설정이 현실과 맞지 않음(프록시, TLS, 리다이렉트)
readinessProbe의 timeoutSeconds 기본은 짧고, 네트워크 경로(서비스 메시 사이드카, 프록시, TLS 핸드셰이크)가 끼면 순간 지연이 생길 수 있습니다. 또한 애플리케이션이 /health에서 301 리다이렉트를 내거나, 인증이 필요한 엔드포인트를 찌르는 경우도 있습니다.
증상
Readiness probe failed: context deadline exceeded- 특정 노드/특정 시간대에만 간헐적으로 발생
개선 체크
- readiness 엔드포인트는 인증 없이, 빠르게, 고정 응답
timeoutSeconds를 실제 p99 지연에 맞게 조정periodSeconds와failureThreshold로 "허용 가능한 흔들림"을 반영
readinessProbe:
httpGet:
path: /readyz
port: 8080
timeoutSeconds: 3
periodSeconds: 5
failureThreshold: 3
successThreshold: 1
7) 종료 신호 처리 미흡으로 롤링 업데이트 중 반복 재시작(Graceful shutdown 문제)
readinessProbe는 트래픽을 끊는 스위치이기도 합니다. 종료 시점에 readiness를 먼저 false로 만들고, 진행 중 요청을 정리한 뒤 종료해야 합니다. 그런데 SIGTERM 처리나 preStop 훅이 없으면, 로드밸런서/서비스 메시가 트래픽을 보내는 동안 프로세스가 죽고, 재시작과 readiness 실패가 함께 보일 수 있습니다.
개선 패턴
terminationGracePeriodSeconds확보preStop에서 짧게 대기해 엔드포인트 드레인- 애플리케이션이 SIGTERM을 받아 readiness를 내리고 작업을 정리
terminationGracePeriodSeconds: 30
containers:
- name: app
image: myapp:1.0.0
lifecycle:
preStop:
exec:
command:
- sh
- -lc
- 'sleep 5'
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 5
실전 트러블슈팅 순서(체크리스트)
kubectl describe pod로 이벤트에서Readiness probe failed의 구체 메시지 확인kubectl logs --previous로 "왜 프로세스가 죽었는지"를 먼저 확인exitCode가137이면 OOM부터 해결- Pod 내부에서
wget또는curl로 readiness 엔드포인트 직접 호출 - DNS/네트워크 의심 시
nslookup과 CoreDNS 상태 점검 - 워밍업이 길면
startupProbe도입 - readiness에 외부 의존성 체크가 과도하면 분리(경량
readyz권장)
예시: 권장 프로브 세트 템플릿
아래는 많은 서비스에 무난하게 적용되는 템플릿입니다.
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 2
failureThreshold: 60
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /livez
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
마무리
readinessProbe 실패는 원인 그 자체라기보다, "서비스가 준비되지 않았다"는 결과 신호인 경우가 많습니다. 따라서 CrashLoopBackOff와 함께 볼 때는 프로브 설정만 만지기보다, 종료 코드, 직전 로그, 리소스, DNS, 의존성 초기화, 종료 시그널 처리를 함께 보아야 재발을 막을 수 있습니다.
특히 EKS에서 DNS가 흔들리는 경우 readiness 실패가 연쇄 장애로 번질 수 있으니, 필요하면 EKS CoreDNS 장애? DNS 타임아웃 8단계 체크리스트로 인프라 레벨도 같이 점검해 보세요.