"앱은 멀쩡한데 Pod가 계속 재시작돼요?" - Probe 설정이 오히려 독이 되는 이유
DevOps 엔지니어라면 한 번쯤 겪어봤을, 가장 당황스러운 상황이 있습니다. 애플리케이션 로그를 확인하면 모든 것이 정상적으로 동작하고, 테스트 환경에서는 완벽하게 작동하는데, 실제 Kubernetes 클러스터에 배포한 Pod만 유독 불안정합니다. kubectl get pods를 확인해보면 CrashLoopBackOff나 Error 상태가 떠 있고, 심지어 Ready 상태가 0/1로 표시되어 트래픽을 전혀 받지 못하는 상황이죠.
대부분의 원인은 애플리케이션 코드의 버그가 아닙니다. 바로 Liveness Probe나 Readiness Probe의 오해에서 비롯됩니다. 이 프로브들은 우리를 보호하기 위해 존재하지만, 잘못 설정하면 오히려 애플리케이션을 죽이는 '가짜 감시자'가 될 수 있습니다.
이 글에서는 단순한 에러 메시지 해석을 넘어, Probe 3형제의 동작 원리를 완벽히 분해하고, 현업에서 가장 빈번하게 발생하는 6가지 실패 원인과 이를 즉시 해결할 수 있는 YAML 처방전까지, 실전적인 가이드를 제공합니다.
Liveness, Readiness, Startup Probe 3형제: 동작 원리 완벽 분해
이 세 가지 프로브는 이름만 비슷할 뿐, Kubernetes 내부에서 수행하는 역할과 실패 시의 영향은 완전히 다릅니다. 이 차이를 이해하는 것이 문제 해결의 80%를 차지합니다.
| Probe 종류 | 목적 | 실패 시 동작 | 주요 영향 |
|---|---|---|---|
| Liveness Probe | 컨테이너가 살아있는지(Alive) 확인 (치명적 오류 감지) | 실패 횟수 초과 시, K8s가 컨테이너를 강제 재시작시킵니다. | Pod의 재시작 횟수(restarts) 증가, 서비스 중단 발생. |
| Readiness Probe | 컨테이너가 트래픽을 받을 준비가 되었는지(Ready) 확인 | 실패 시, 해당 Pod의 IP를 Service Endpoint에서 제외합니다. (재시작하지 않음) | 트래픽이 Pod로 라우팅되지 않아 503 Service Unavailable 발생. |
| Startup Probe | 컨테이너가 초기 부팅을 완료했는지 확인 | 실패 횟수 초과 시, Liveness/Readiness Probe 검사를 시작합니다. | 느린 부팅 앱이 Liveness/Readiness Probe에 의해 강제 종료되는 것을 방지합니다. |
핵심 이해:
- Liveness 실패 $\rightarrow$ 재시작 (Restart)
- Readiness 실패 $\rightarrow$ 트래픽 차단 (Service Endpoint 제외)
- Startup 실패 $\rightarrow$ 대기 후 검사 시작 (Liveness/Readiness 검사 자체를 지연)
🚨 진단 첫 단계: kubectl describe로 에러 메시지 읽는 법
문제가 생겼을 때 가장 먼저 해야 할 일은 감(感)이 아니라 증거(Evidence)를 찾는 것입니다. kubectl get pod <pod-name>에서 READY 컬럼이 0/1이거나, RESTARTS가 비정상적으로 높은 경우, 즉시 kubectl describe pod <pod-name>을 실행하세요.
여기서 주목해야 할 섹션은 Events와 Conditions입니다.
실제 에러 메시지 예시 및 해석:
- Liveness 실패 예시:
Liveness probe failed: HTTP probe failed with statuscode: 503- 해석: 컨테이너는 살아있지만(재시작되지 않았을 수 있음),
/healthz엔드포인트가 503을 반환하며 "지금은 트래픽을 받을 수 없다"고 명시적으로 말하고 있습니다. (→ Readiness Probe 문제일 가능성 높음)
- 해석: 컨테이너는 살아있지만(재시작되지 않았을 수 있음),
- Readiness 실패 예시:
Readiness probe failed: Get "http://10.x.x.x:8080/healthz": dial tcp connection refused- 해석: 컨테이너가 해당 포트(8080)에서 아예 연결을 거부당했습니다. 포트가 열려있지 않거나, 앱이 아직 해당 포트를 바인딩하지 못한 상태입니다. (→
initialDelaySeconds부족 또는 포트 오타 의심)
- 해석: 컨테이너가 해당 포트(8080)에서 아예 연결을 거부당했습니다. 포트가 열려있지 않거나, 앱이 아직 해당 포트를 바인딩하지 못한 상태입니다. (→
- Startup 실패 예시:
Startup probe failed: ...- 해석: 부팅이 너무 느려서, Liveness/Readiness 검사가 시작되기도 전에 부팅 자체가 실패하고 있습니다. (→
startupProbe추가가 시급합니다.)
- 해석: 부팅이 너무 느려서, Liveness/Readiness 검사가 시작되기도 전에 부팅 자체가 실패하고 있습니다. (→
🛠️ 현업에서 만나는 6대 Probe 실패 원인과 YAML 처방전
실제 운영 환경에서 가장 많이 부딪히는 6가지 시나리오를 '증상 $\rightarrow$ 원인 $\rightarrow$ 해결' 구조로 정리했습니다.
1. 초기 지연 시간 부족 (InitialDelaySeconds 부족)
- 증상: 배포 직후 Pod가 즉시 실패하고 재시작 루프에 빠집니다.
- 원인: 애플리케이션이 JVM 로딩이나 DB 연결 초기화에 시간이 걸리는데, Probe가 너무 빨리 검사를 시작합니다.
- 해결:
initialDelaySeconds를 충분히 늘려줍니다. (예: 30초)
2. 포트 또는 Path 오타/불일치
- 증상:
connection refused또는unknown endpoint에러가 발생합니다. - 원인: YAML에 명시된 포트(예: 8080)와 실제 앱이 리스닝하는 포트가 다릅니다.
- 해결:
kubectl exec으로 직접curl http://localhost:8080/healthz를 실행하여 정확한 포트와 경로를 확인하고 YAML을 수정합니다.
3. Status Code의 함정 (200 OK가 아닐 때)
- 증상: Probe는 통신에 성공했으나, 계속 실패하여 Pod가 불안정합니다.
- 원인: 헬스체크 엔드포인트가 성공적으로 응답하더라도, 3xx 리다이렉트나 401(인증 필요) 같은 코드를 반환하면 Probe는 이를 '실패'로 간주합니다.
- 해결: 헬스체크 경로는 반드시 200 OK를 반환하도록 애플리케이션 레벨에서 수정해야 합니다.
4. 외부 의존성 대기 (DB/Redis 등)
- 증상: Pod는 재시작하지 않지만, 트래픽을 받지 못하고 503 에러가 발생합니다.
- 원인: 앱이 시작되자마자 DB 연결을 시도하는데, DB가 아직 준비되지 않아 연결이 실패합니다.
- 해결: Readiness Probe를 사용하되, 이 Probe가 DB 연결 성공 여부를 확인하도록 로직을 분리해야 합니다. (가장 이상적인 패턴)
5. 과도한 부하로 인한 타임아웃 (Timeout/PeriodSeconds)
- 증상: 평소엔 괜찮다가 트래픽이 몰리면 갑자기 실패합니다.
- 원인:
timeoutSeconds가 너무 짧으면, 일시적으로 부하가 걸려 응답 시간이 1초를 넘길 때마다 Probe가 실패 처리됩니다. - 해결:
timeoutSeconds를 충분히 늘리거나,periodSeconds를 늘려 검사 빈도를 낮춥니다.
6. 느린 부팅 앱의 사망 (Startup Probe 부재)
- 증상: JVM 기반 앱이나 마이그레이션 로직이 복잡한 앱에서만 간헐적으로 재시작이 발생합니다.
- 원인: Liveness/Readiness Probe가 부팅 완료 전에 너무 빨리 실행되어 앱을 강제로 종료시킵니다.
- 해결: 반드시
startupProbe를 사용하여 부팅 완료를 위한 '면죄부 시간'을 확보해야 합니다.
💡 실전 YAML 처방전: Probe 3종 조합 예시
다음은 가장 권장되는 조합의 YAML 예시입니다. (HTTP Get 방식 기준)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-deployment
spec:
template:
spec:
containers:
- name: my-container
image: your-registry/my-app:latest
ports:
- containerPort: 8080
# 1. Startup Probe: 부팅 완료까지 최대 120초 허용 (12초 * 10회)
startupProbe:
httpGet:
path: /bootstrap-ready # 부팅 전용 경로
port: 8080
failureThreshold: 10 # 최대 10번 실패 허용
periodSeconds: 12 # 12초마다 검사
# 2. Readiness Probe: 트래픽 수신 준비 완료 시점 체크
readinessProbe:
httpGet:
path: /readyz # 트래픽 수신 준비 경로
port: 8080
initialDelaySeconds: 30 # 30초 대기 후 검사 시작
periodSeconds: 10
failureThreshold: 3
# 3. Liveness Probe: 치명적 오류 감지용 (최후의 보루)
livenessProbe:
httpGet:
path: /livez # 생존 여부만 확인하는 경량 경로
port: 8080
initialDelaySeconds: 60 # 가장 늦게 검사 시작
periodSeconds: 20
failureThreshold: 5📌 Startup Probe 최대 허용 시간 계산 팁:
최대 허용 시간 $\approx$ failureThreshold $\times$ periodSeconds
(위 예시: $10 \times 12 = 120$초)
🚀 무중단 배포를 위한 고급 패턴: PreStop Hook 활용
단순히 Pod가 준비되는 것만으로는 부족합니다. 롤링 업데이트 과정에서 클라이언트가 연결을 끊는 순간(Connection Drop)을 막아야 합니다. 이때 preStop 훅을 활용합니다.
preStop 훅은 K8s가 Pod를 종료하기 직전에 실행되는 스크립트입니다. 이 시점에 애플리케이션에게 "이제 곧 종료할 테니, 들어오는 연결을 받지 말고 점진적으로 연결을 끊어라"라는 신호를 줄 수 있습니다.
lifecycle:
preStop:
exec:
command: ["/bin/sleep", "10"] # 10초 동안 대기하며 연결 종료 유도preStop으로 시간을 벌고, 동시에 readinessProbe가 실패하도록 유도하면, Service는 이 Pod로의 트래픽 전송을 즉시 중단하여 502/503 에러를 최소화할 수 있습니다.
⏱️ 5분 진단 체크리스트: Probe 문제 해결 순서
문제가 발생했을 때 이 순서대로 점검하면 90% 이상 해결됩니다.
kubectl describe pod확인:Events에서 정확한 에러 메시지(503, connection refused 등)를 확보합니다.initialDelaySeconds점검: 앱의 실제 부팅 시간을 고려하여 충분한 시간을 주었는지 확인합니다.kubectl exec로 직접 검증:kubectl exec -it <pod-name> -- curl http://localhost:8080/path를 실행하여, YAML에 명시된 포트/Path가 정말 작동하는지 확인합니다.- Probe 분리 및 점검:
- 느린 앱? $\rightarrow$
startupProbe추가 (가장 먼저). - 트래픽 차단 문제? $\rightarrow$
readinessProbe의 로직(DB 연결 등)을 분리하여 검증. - 치명적 오류? $\rightarrow$
livenessProbe의 경량화 및 재검토.
- 느린 앱? $\rightarrow$
- 의존성 분리: 외부 의존성(DB)이 있다면, 해당 의존성 체크 로직을
readinessProbe에만 포함시키고,livenessProbe는 단순한 메모리 체크 등으로 분리합니다.
실무자 경험 공유:
초기에는 Liveness Probe와 Readiness Probe를 같은 /health 엔드포인트에 걸어버리는 실수를 자주 했습니다. 그 결과, DB 연결이 잠시 끊기면 Liveness Probe가 실패하여 Pod가 재시작되고, 이 재시작 과정 자체가 클라이언트에게는 '서비스 중단'으로 인식되는 악순환이 발생했습니다. 가장 중요한 원칙은 '생존(Liveness)'과 '서비스 준비(Readiness)'의 로직을 분리하는 것입니다.
자주 묻는 질문 (FAQ)
Q. Liveness Probe와 Readiness Probe를 둘 다 설정하면 어떻게 되나요? A. 두 프로브 모두 실패할 경우, Liveness Probe가 먼저 실패하면 Pod가 재시작되므로 서비스가 중단됩니다. Readiness Probe가 실패하면 트래픽만 차단됩니다. 따라서 두 프로브의 실패 원인과 영향 범위를 명확히 분리하여 설정해야 합니다.
Q. failureThreshold를 높게 설정하는 것이 항상 좋은가요?
A. 아닙니다. failureThreshold는 '실패해도 괜찮은 횟수'를 의미합니다. 너무 높게 설정하면 실제 장애가 발생했을 때 복구 시간이 길어지므로, 앱의 특성과 허용 가능한 장애 시간을 고려하여 적정 수준(보통 3~5회)으로 설정하는 것이 좋습니다.
Q. readinessProbe가 실패하면 Pod는 재시작되나요?
A. 아닙니다. Readiness Probe가 실패하면 K8s는 해당 Pod를 Service의 Endpoint 목록에서 제외(Deregister)할 뿐, 컨테이너를 재시작시키지 않습니다. 재시작은 Liveness Probe의 역할입니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...