본문으로 건너뛰기
4.2 Pod 생명주기와 프로브

4.2 Pod 생명주기와 프로브

kubectl get pods의 STATUS 열은 서로 다른 계층의 정보가 섞여 표시되는 자리다. Running으로 표시되는데 트래픽을 받지 못하는 Pod가 있고, 자주 마주치는 CrashLoopBackOff라는 값은 Pod phase 목록 어디에도 없다. 이 장은 Pod의 phase와 컨테이너 상태를 구분하는 데서 시작해, kubelet이 컨테이너의 건강을 판정하는 세 가지 probe, 그리고 Pod가 종료되는 순서를 따라간다.

phase와 컨테이너 상태는 다른 층이다

Pod의 phase는 생애 전체를 다섯 값으로 요약한 것이다.

phase의미
Pending클러스터가 수락했지만 아직 실행 전. 스케줄 대기와 이미지 pull 포함
Running노드에 배정됐고 컨테이너가 최소 하나 실행 중이거나 재시작 중
Succeeded모든 컨테이너가 성공 종료했고 재시작되지 않음
Failed모든 컨테이너가 종료했고 최소 하나가 실패로 종료
Unknown노드와의 통신 두절 등으로 상태 조회 불가

다섯 값뿐이라 상세를 담지 못하고, 상세는 컨테이너마다 붙는 상태에 있다. 각 컨테이너는 Waiting, Running, Terminated 셋 중 하나이고 Waiting과 Terminated에는 reason이 따라온다. 이미지 pull이 계속 실패하는 컨테이너는 Waiting에 reason ImagePullBackOff, crash를 반복하는 컨테이너는 Waiting에 reason CrashLoopBackOff다. STATUS 열의 CrashLoopBackOff는 phase가 아니라 컨테이너 상태의 reason이며, 이때 Pod의 phase는 여전히 Running이다. 표시가 혼란스러우면 kubectl describe pod-o yamlstatus.containerStatuses를 직접 확인하는 편이 정확하다.

재시작의 주체는 control plane이 아니라 그 노드의 kubelet이다. Pod의 restartPolicy(기본 Always, OnFailure, Never)가 모든 컨테이너에 적용되고, kubelet은 crash한 컨테이너를 10초에서 시작해 두 배씩 늘어나는 지연(최대 5분)을 두고 같은 자리에서 재시작한다. CrashLoopBackOff의 BackOff가 바로 이 지연이다. 컨테이너가 10분 동안 문제없이 실행되면 지연은 초기화된다.

세 가지 probe: 오판의 비용이 각각 다르다

kubelet은 컨테이너에 주기적으로 검사를 실행해 건강을 판정한다. 검사 방식은 exec, httpGet, tcpSocket, grpc 네 가지이고, 어느 방식을 쓰든 검사의 종류가 셋으로 나뉜다. 같은 검사라도 종류에 따라 실패의 결과가 완전히 다르다.

probe실패 시 동작목적
liveness컨테이너 kill 후 restartPolicy 적용회복 불가능한 상태에서 탈출
readinessService endpoint에서 제외, 재시작 없음트래픽 수신 가능 여부 표시
startup컨테이너 kill 후 restartPolicy 적용기동이 느린 프로세스 보호

readiness는 회복을 전제한다. 실패해도 컨테이너를 종료하지 않고 트래픽만 차단하며, 다시 성공하면 endpoint에 복귀한다. 워밍업, 일시적 과부하, 종료 준비 같은 상태를 표현하는 자리다. liveness는 회복 불가를 전제한다. 실패가 failureThreshold(기본 3회) 연속되면 kubelet이 컨테이너를 종료시킨다. 재시작 말고는 처방이 없는 교착이나 무한 대기에서 프로세스를 복구하는 수단이다.

startup probe는 다른 둘의 앞에 선다. startup probe가 정의되어 있으면 그것이 성공할 때까지 liveness와 readiness는 실행되지 않는다. 초기 데이터 로딩이나 recovery 때문에 기동에 수 분이 걸리는 프로세스에 liveness를 그대로 붙이면 기동이 끝나기 전에 종료되는데, 그 구간을 startup probe가 대신 맡는다. failureThreshold: 30periodSeconds: 10이면 최대 300초의 기동 시간을 허용하는 식이다.

liveness probe에 외부 의존성 검사를 넣지 않는다. 검사 핸들러가 데이터베이스 연결까지 확인하도록 만들면, 데이터베이스 장애 순간 그 DB를 바라보는 모든 Pod가 동시에 liveness 실패로 재시작된다. 재시작은 외부 장애를 고치지 못하므로 재시작 폭풍만 반복되고, 커넥션과 캐시가 초기화된 채 일제히 재기동하는 부하가 장애를 오히려 연장한다. liveness는 프로세스 자신의 상태만 검사하고, 의존성의 문제는 readiness로 표현한다.

종료의 순서: preStop, SIGTERM, SIGKILL

Pod 삭제가 요청되면 Pod는 Terminating으로 표시되고 terminationGracePeriodSeconds(기본 30초)의 카운트다운이 시작된다. 이때 두 갈래가 병행된다. 한쪽에서는 Pod가 Service의 endpoint 목록에서 빠지기 시작하고, 다른 쪽에서는 kubelet이 종료 절차에 들어간다. preStop hook이 정의되어 있으면 먼저 실행하고, 그다음 각 컨테이너의 주 프로세스(PID 1)에 SIGTERM을 보낸다. grace period가 다 지나도 프로세스가 살아 있으면 SIGKILL로 강제 종료한다. preStop의 실행 시간도 같은 grace period에서 차감되며, preStop이 시간을 다 쓰면 2초의 짧은 연장 뒤 강제 종료로 넘어간다.

    flowchart TD
  A["삭제 요청 (Terminating)"] --> B["endpoint 제거 시작"]
  A --> C["preStop hook 실행"]
  C --> D["SIGTERM 전달 (PID 1)"]
  D --> E{"grace period 내 종료?"}
  E -->|"예"| F["Pod 제거"]
  E -->|"아니오"| G["SIGKILL 강제 종료"]
  G --> F
  

운영에서 걸리는 지점이 두 곳이다. 첫째, endpoint 제거와 SIGTERM 전달은 서로를 기다리지 않는다. SIGTERM을 받자마자 소켓을 닫는 애플리케이션은 아직 endpoint 전파가 끝나지 않아 계속 들어오는 요청을 끊어 버린다. preStop에 몇 초의 sleep을 두어 전파를 기다리는 관행이 여기서 나왔다. 둘째, SIGTERM은 PID 1에만 간다. sh -c "..." 같은 shell 경유 entrypoint는 shell이 PID 1이 되어 시그널을 자식 프로세스에 전달하지 않으므로, 애플리케이션은 SIGTERM을 받지 못한 채 30초 뒤 SIGKILL로 강제 종료된다. 버퍼를 정리하며 내려가야 하는 데이터베이스 계열 프로세스라면 exec 형태의 entrypoint와 넉넉한 terminationGracePeriodSeconds가 전제 조건이다.

정리

phase는 Pod 생애의 다섯 단계 요약이고, 실제 진단 정보는 컨테이너 상태(Waiting/Running/Terminated)와 그 reason에 있다. 세 probe는 실패의 결과가 서로 다르므로 목적에 맞게 나누어 쓰되 liveness에는 자기 프로세스의 건강만 담는다. 종료는 preStop, SIGTERM, SIGKILL 순서를 따르며, 전체가 grace period 하나의 제한 시간 안에서 진행된다.