왜 프로브가 필요한가
컨테이너 프로세스가 살아 있다고 해서 트래픽을 받을 준비가 된 것은 아니다. JVM은 워밍업 중이고, DB 커넥션 풀은 아직 비어 있으며, 마이그레이션이 끝나지 않았을 수 있다. 반대로 프로세스는 떠 있지만 데드락이나 이벤트 루프 블로킹으로 응답하지 못하는 "좀비" 상태도 흔하다. 쿠버네티스는 이런 상태를 애플리케이션 내부 로직만으로 판단할 수 없기 때문에, 컨테이너가 직접 자신의 건강 상태를 노출하도록 프로브(probe) 메커니즘을 제공한다.
3종 프로브의 역할 구분
가장 흔한 실수가 세 프로브의 목적을 혼동하는 것이다. 각각의 실패는 완전히 다른 결과를 낳는다.
| 프로브 | 실패 시 동작 | 목적 |
|---|---|---|
| startup | 다른 프로브 시작을 지연, 최종 실패 시 재시작 | 느린 초기화 보호 |
| liveness | 컨테이너 재시작 | 복구 불가능한 상태 감지 |
| readiness | Service 엔드포인트에서 제외 | 일시적으로 트래픽 차단 |
핵심은 이것이다. liveness 실패는 "죽여서 다시 살려라", readiness 실패는 "살아 있지만 지금은 보내지 마라". 이 둘을 같은 엔드포인트로 묶으면 재앙이 된다.
가장 흔한 사고: liveness가 DB를 검사할 때
liveness 프로브에서 DB나 외부 API 상태를 검사하면, DB가 잠깐 느려지는 순간 모든 파드가 동시에 재시작을 시작한다. 재시작한 파드들이 다시 DB에 몰리면서 장애가 증폭되는 캐스케이딩 장애가 발생한다. liveness는 오직 자기 자신만 검사해야 한다. 외부 의존성은 readiness의 영역이다.
실전 매니페스트 예시
livenessProbe:
httpGet:
path: /healthz # 의존성 검사 없음. 프로세스 응답만 확인
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3 # 30초간 무응답이면 재시작
readinessProbe:
httpGet:
path: /readyz # DB/캐시 커넥션까지 확인
port: 8080
periodSeconds: 5
failureThreshold: 2
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 30 # 최대 150초까지 초기화 대기
startupProbe가 성공하기 전까지 liveness와 readiness는 아예 실행되지 않는다. 따라서 느린 부팅 애플리케이션에서 initialDelaySeconds로 대충 큰 값을 박아 넣는 대신, startupProbe로 초기화 구간과 정상 운영 구간을 분리하는 것이 정석이다.
엔드포인트를 분리해서 구현하기
애플리케이션 코드에서 /healthz와 /readyz를 반드시 나눈다. 아래는 FastAPI 예시다.
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/healthz") # liveness: 프로세스가 응답하는지만
def healthz():
return {"status": "ok"}
@app.get("/readyz") # readiness: 실제 의존성 확인
def readyz(response: Response):
try:
db.execute("SELECT 1") # 커넥션 살아있는지
cache.ping()
return {"status": "ready"}
except Exception:
response.status_code = 503 # 트래픽에서 제외됨
return {"status": "not ready"}
readyz는 503을 반환해도 파드가 죽지 않고 단지 Service에서 빠질 뿐이라는 점이 중요하다. DB가 회복되면 자동으로 다시 엔드포인트에 등록된다.
graceful shutdown과 readiness
파드가 종료될 때 kubelet의 엔드포인트 제거와 SIGTERM 전달은 비동기로 일어난다. 그래서 종료 신호를 받은 뒤에도 몇 초간 새 트래픽이 들어올 수 있다. preStop 훅에 짧은 sleep을 넣어 엔드포인트가 완전히 빠질 시간을 벌어주면 무중단 배포에서 커넥션 리셋 에러를 줄일 수 있다.
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 30
주의점 정리
- liveness에 외부 의존성을 절대 넣지 마라. 캐스케이딩 재시작의 주범이다.
timeoutSeconds기본값은 1초다. 부하가 걸리면 정상 파드도 타임아웃으로 재시작될 수 있으니 여유를 둔다.- startupProbe의
failureThreshold × periodSeconds가 실제 최대 부팅 시간보다 커야 한다. - readiness가 계속 실패하면 트래픽이 다른 파드로 몰린다. 전체 파드가 동시에 readiness 실패에 빠지지 않도록 의존성 검사 로직을 방어적으로 작성한다.