Published on

K8s CrashLoopBackOff - readinessProbe 실패 7원인

Authors

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의 고전 원인입니다. 메모리가 부족하면 exitCode137로 남고, 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 지연에 맞게 조정
  • periodSecondsfailureThreshold로 "허용 가능한 흔들림"을 반영
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

실전 트러블슈팅 순서(체크리스트)

  1. kubectl describe pod로 이벤트에서 Readiness probe failed의 구체 메시지 확인
  2. kubectl logs --previous로 "왜 프로세스가 죽었는지"를 먼저 확인
  3. exitCode137이면 OOM부터 해결
  4. Pod 내부에서 wget 또는 curl로 readiness 엔드포인트 직접 호출
  5. DNS/네트워크 의심 시 nslookup과 CoreDNS 상태 점검
  6. 워밍업이 길면 startupProbe 도입
  7. 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단계 체크리스트로 인프라 레벨도 같이 점검해 보세요.