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 yaml로 status.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 적용 | 회복 불가능한 상태에서 탈출 |
| readiness | Service endpoint에서 제외, 재시작 없음 | 트래픽 수신 가능 여부 표시 |
| startup | 컨테이너 kill 후 restartPolicy 적용 | 기동이 느린 프로세스 보호 |
readiness는 회복을 전제한다. 실패해도 컨테이너를 종료하지 않고 트래픽만 차단하며, 다시 성공하면 endpoint에 복귀한다. 워밍업, 일시적 과부하, 종료 준비 같은 상태를 표현하는 자리다. liveness는 회복 불가를 전제한다. 실패가 failureThreshold(기본 3회) 연속되면 kubelet이 컨테이너를 종료시킨다. 재시작 말고는 처방이 없는 교착이나 무한 대기에서 프로세스를 복구하는 수단이다.
startup probe는 다른 둘의 앞에 선다. startup probe가 정의되어 있으면 그것이 성공할 때까지 liveness와 readiness는 실행되지 않는다. 초기 데이터 로딩이나 recovery 때문에 기동에 수 분이 걸리는 프로세스에 liveness를 그대로 붙이면 기동이 끝나기 전에 종료되는데, 그 구간을 startup probe가 대신 맡는다. failureThreshold: 30에 periodSeconds: 10이면 최대 300초의 기동 시간을 허용하는 식이다.
종료의 순서: 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 하나의 제한 시간 안에서 진행된다.