헬스체크 하나 잘못 설계하면 쿠버네티스 클러스터 전체가 흔들리는가?

결론부터 말하면 그렇다. liveness probe에 외부 의존성(DB, 캐시) 체크를 넣으면, 그 의존성이 잠깐 흔들릴 때 "죽지도 않은 프로세스"가 무더기로 재시작당한다. 이 글은 판교의 한 결제 연동 서비스에서 실제로 겪은 장애를 복기한 것이다.

  • liveness는 "프로세스가 복구 불가능한 상태인가"만 판단해야 하고, readiness는 "지금 트래픽을 받아도 되는가"를 판단해야 한다 — Kubernetes 공식 문서도 이 둘을 별개 신호로 정의한다.
  • 이 분리를 하지 않으면 외부 장애 하나가 재시작 폭주로 증폭된다.
  • 서비스 수가 적고 팀이 작다면, 이런 probe 설계 실수의 파급력 자체가 작아서 단순 컨테이너 런타임으로도 충분할 수 있다.
  • 서비스가 많고 트래픽 변동성이 크다면 readiness/liveness를 세밀하게 나누는 설계 역량이 오히려 쿠버네티스를 쓰는 이유가 된다.

당시 상황은 이랬다. 결제 연동 서비스는 파드 12개로 돌고 있었고, 뒷단 RDS가 예정된 마이너 버전 패치로 약 40초간 페일오버를 거쳤다. 그 40초 동안 서비스 전체가 502를 뱉었고, 복구 이후에도 5분 넘게 파드가 계속 재시작되며 에러율이 널뛰었다. 원래는 40초짜리 순단으로 끝났어야 할 이벤트가 왜 5분짜리 장애로 늘어났는지가 이 회고의 핵심이다.

왜 DB 페일오버 중에 파드가 무더기로 재시작됐나?

원인은 liveness probe 설정에 있었다. /healthz 엔드포인트가 애플리케이션 프로세스 생존 여부뿐 아니라 DB 커넥션 풀에 ping을 날려 응답하도록 짜여 있었고, 이 엔드포인트를 liveness와 readiness 양쪽에 동일하게 물려놓았다.

RDS 페일오버가 시작되자 DB ping이 타임아웃(설정값 3초)나기 시작했고, /healthz는 500을 반환했다. failureThreshold 3, periodSeconds 10으로 설정돼 있었으니 30초 안에 liveness 실패가 확정됐다. kubelet은 이걸 "프로세스가 죽었다"로 해석하고 컨테이너를 강제 종료 후 재시작했다. 문제는 재시작한 새 컨테이너도 기동 직후 다시 같은 /healthz를 호출했고, DB가 아직 페일오버 중이라 또 실패했다는 점이다. 이 사이클이 파드 12개 전체에서 거의 동시에 돌면서, DB가 정상화된 이후에도 파드들이 서로 다른 타이밍에 재시작 큐에 걸려 있어 5분 가까이 서비스가 정상화되지 못했다.

liveness probe에 DB 체크가 들어간 게 정말 원인이었나?

가설을 검증하려면 재시작 시각과 DB 페일오버 시각을 겹쳐봐야 했다. 파드 이벤트 로그(kubectl get events)를 뽑아보니 Liveness probe failed 이벤트 31건이 페일오버 시작 후 28초~33초 사이에 몰려 있었고, 이 시각은 RDS 이벤트 로그의 페일오버 완료 시각(약 40초 지점)보다 앞서 있었다. 즉 DB가 아직 완전히 복구되지 않은 시점에 liveness가 먼저 실패를 확정한 것이다.

readiness probe 이벤트도 같은 시각대에 실패가 찍혀 있었는데, 이건 정상적인 동작이었다. readiness가 실패해 트래픽이 차단된 건 의도한 설계였고, 문제는 거기에 더해 liveness까지 같은 조건으로 실패해 컨테이너 자체가 죽어버린 것이었다. 이 지점에서 "readiness만으로 충분했을 상황을 liveness가 겹쳐서 증폭시켰다"는 가설이 확정됐다.

재현 테스트에서 무엇을 확인했나?

스테이징 환경에서 RDS 대신 iptables로 DB 포트를 25초간 차단해 페일오버를 흉내냈다. liveness와 readiness가 동일한 /healthz를 쓰는 기존 구성에서는 파드 6개 중 5개가 재시작 이벤트를 발생시켰고, 서비스 정상화까지 약 2분 10초가 걸렸다. 반면 liveness probe를 프로세스 생존만 확인하는 /livez(단순 200 OK, DB 체크 없음)로 바꾸고 readiness만 DB ping을 포함한 /readyz로 분리한 구성에서는, 같은 25초 차단 조건에서 재시작 이벤트가 0건이었다. 파드는 unready 상태로 트래픽만 차단됐다가, DB 포트가 열리자마자 8초 안에 다시 ready로 돌아왔다. 이 재현 결과로 "liveness에 DB 체크를 포함한 것"이 재시작 폭주의 직접 원인이라는 게 확정됐다.

어떻게 바꿨고 어떤 지표로 확인했나?

liveness와 readiness 엔드포인트를 완전히 분리했다. liveness(/livez)는 프로세스 내부 헬스 체크 루프가 정상적으로 도는지만 확인하고 외부 호출을 하지 않는다. readiness(/readyz)만 DB 커넥션 풀 상태, 캐시 연결 상태를 확인한다. startup probe도 별도로 추가해 기동 시 커넥션 풀 초기화에 걸리는 최대 15초 동안은 liveness 판단 자체를 유예했다. 이 구조는 Kubernetes probe 설정 가이드에서 제시하는 세 probe 역할 분리와 같은 방향이다.

변경 이후 두 달간 실제로 발생한 RDS 마이너 패치 이벤트 2건을 관찰했는데, 두 번 모두 liveness 실패 이벤트는 0건, readiness 실패로 인한 트래픽 차단만 각각 22초, 19초 발생했고 컨테이너 재시작은 없었다. 서비스 정상화 시간도 DB 복구 시각과 거의 일치했다.

같은 함정을 피하려면 기본값을 어떻게 잡아야 하나?

liveness는 기본값을 "외부 의존성을 절대 넣지 않는다"로 잡는 편이 안전하다. 재시작으로 고칠 수 없는 문제(데드락, 무한 루프, 메모리 누수로 인한 응답 불가)에만 반응하도록 좁게 설계하고, DB·캐시·외부 API 상태는 전부 readiness로 보낸다. 이 원칙은 CNCF의 2025년 운영 사례 정리에서 지적하는 "숨은 운영 컴포넌트 관리 부담"과도 맞닿아 있다 — probe 하나가 DNS, 네트워킹, 모니터링 같은 다른 구성요소의 불안정성을 컨테이너 재시작으로 잘못 전파시키지 않도록 경계를 명확히 그어야 한다.

그리고 이 판단은 플랫폼 선택 이전 단계의 문제라는 점도 짚어둘 만하다. 서비스가 하나뿐이고 트래픽이 안정적이라면 이런 probe 분리 없이도 단순 컨테이너 런타임이나 매니지드 플랫폼으로 충분히 버틸 수 있다. 하지만 서비스 수가 늘고 팀이 나뉘면, readiness/liveness를 세밀하게 나누는 역량 자체가 쿠버네티스를 쓰는 이유가 된다. Red Hat의 2026년 플릿 관리 조사에서 응답자의 61%가 일관된 설정·정책 강제의 어려움을 꼽은 것도, 결국 이런 probe 설계 원칙을 여러 서비스에 걸쳐 동일하게 유지하는 게 기술 문제가 아니라 운영 일관성 문제라는 걸 보여준다.

핵심 정리

  • liveness에 외부 의존성 체크를 넣으면, 의존성 장애가 컨테이너 재시작 폭주로 증폭될 수 있다.
  • readiness는 트래픽 차단, liveness는 재시작이라는 서로 다른 결과를 낳으므로 반드시 역할을 분리해 설계해야 한다.
  • 기동이 느린 앱은 startup probe로 초기화 구간의 liveness 오탐을 별도로 유예하는 편이 안전하다.
  • 이런 probe 설계 복잡도는 서비스 수·팀 규모가 커질수록 감당할 가치가 커지고, 반대의 경우 단순 컨테이너 런타임이나 매니지드 플랫폼이 더 실용적일 수 있다.
  • 재현 테스트(포트 차단 등)로 재시작 이벤트 유무를 직접 확인하는 것이 원인 확정의 가장 확실한 방법이다.