Kubernetes의 Liveness, Readiness, Startup Probe
1. 개요
Kubernetes cluster에서 애플리케이션을 실행할 때, cluster는 당신의 pod가 “살아있는지” 또는 “트래픽을 받을 준비가 되었는지” 자동으로 알 수 없습니다. probe를 통해 문의해야 합니다. 이 글은 liveness, readiness, startup probe 사이의 차이점을 설명하고, Spring Boot Actuator 설정 예제와 Deployment YAML을 제공합니다. 이 글을 읽고 나면, OrderService와 같은 실제 서비스에 대해 올바르게 probe를 설정할 수 있으며, pod가 불필요한 restart loop에 빠지지 않도록 할 수 있을 것입니다.
2. 왜 probe가 필요한가
기본적으로 Kubernetes는 container 내의 process가 실행 중인지 여부(container 상태가 Running인지)만 알 수 있습니다. 내부의 애플리케이션에 대해서는 알 수 없습니다 — deadlock에 빠졌는지, 데이터베이스 연결을 잃었는지, 아직 시작이 완료되지 않았는지 모릅니다.
probe가 없을 경우의 결과:
- Pod가 (deadlock으로) 멈춰있지만 여전히 트래픽을 받으면 → 요청 타임아웃이 대량으로 발생합니다.
- Pod가 방금 시작되어 아직 DB 연결을 완료하지 못했는데 이미 Service의 endpoint 목록에 추가되었다면 → 500 에러가 발생합니다.
세 가지 probe 유형은 세 가지 다른 문제를 해결합니다:
| Probe | 답변 질문 | 실패했을 때 |
|---|---|---|
| Liveness | Container가 여전히 살아있는가? | Kubernetes가 container를 재시작합니다 |
| Readiness | Container가 트래픽을 받을 준비가 되었는가? | Kubernetes가 pod를 Service endpoint에서 제거합니다 (재시작하지 않음) |
| Startup | Container의 시작이 완료되었는가? | Kubernetes가 대기하고, liveness/readiness를 임시로 비활성화합니다 |
3. Liveness probe
Liveness probe는 다음에 답변합니다: “이 process가 자체 복구 불가능한 상태에 있는가?”. failureThreshold 횟수 이상 probe가 실패하면, kubelet이 container를 종료하고 재시작합니다.
3.1. Spring Boot Actuator 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoint.health.probes.enabled=true
management.endpoints.web.exposure.include=health,info
management.endpoint.health.probes.enabled=true 줄은 두 개의 하위 endpoint를 활성화합니다: /actuator/health/liveness와 /actuator/health/readiness — 이것은 Kubernetes가 예상하는 표준 형식입니다.
3.2. Deployment에 선언
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
template:
spec:
containers:
- name: order-service
image: nonladev/order-service:1.0
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
failureThreshold: 3
initialDelaySeconds: 20: container가 시작된 후 20초를 기다린 다음 probe를 시작합니다.periodSeconds: 10: 10초마다 한 번씩 probe를 호출합니다.failureThreshold: 3: 3번 연속 실패하면 종료된 것으로 간주하여 재시작합니다.
주의: Liveness probe는 “애플리케이션이 여전히 실행 중인가”만 확인해야 합니다. 외부 dependency (DB, Redis 등)를 확인해서는 안 됩니다. DB가 죽고 liveness가 이에 따라 실패하면, Kubernetes는 계속해서 pod를 재시작할 것이며 아무것도 해결할 수 없습니다 — 이것은 매우 흔한 실수입니다.
4. Readiness probe
Readiness probe는 다음에 답변합니다: “pod가 요청을 처리할 준비가 되었는가?”. 실패하면 pod가 Service의 endpoint에서 제거됩니다 — 트래픽이 더 이상 전송되지 않습니다. 하지만 container는 재시작되지 않습니다.
4.1. Custom readiness indicator
OrderService가 PaymentGateway에 연결되어야 한다고 가정합시다:
@Component
public class PaymentGatewayReadinessIndicator implements HealthIndicator {
private final PaymentGatewayClient paymentGatewayClient;
public PaymentGatewayReadinessIndicator(PaymentGatewayClient paymentGatewayClient) {
this.paymentGatewayClient = paymentGatewayClient;
}
@Override
public Health health() {
if (paymentGatewayClient.isConnected()) {
return Health.up().withDetail("gateway", "connected").build();
}
return Health.down().withDetail("gateway", "unreachable").build();
}
}
이 indicator를 readiness 그룹으로 병합하려면 (liveness에는 영향을 주지 않도록), 그룹을 선언합니다:
management.endpoint.health.group.readiness.include=readinessState,paymentGateway
health()가Health.down()을 반환합니다:PaymentGatewayClient가 아직 연결되지 않았을 때 — readiness probe가 실패합니다. pod는 gateway가 준비될 때까지 트래픽을 받지 않습니다.- Spring은 자동으로 bean
paymentGatewayReadinessIndicator의 이름을 health 응답에서paymentGateway키로 지정합니다.
4.2. Indicator 테스트
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
class PaymentGatewayReadinessIndicatorTest {
@Test
void shouldReportDownWhenGatewayNotConnected() {
PaymentGatewayClient client = mock(PaymentGatewayClient.class);
when(client.isConnected()).thenReturn(false);
PaymentGatewayReadinessIndicator indicator = new PaymentGatewayReadinessIndicator(client);
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).containsEntry("gateway", "unreachable");
}
}
mock(PaymentGatewayClient.class)은 외부 dependency를 mock합니다. 실제 연결이 필요하지 않습니다.assertThat(health.getStatus())는 gateway가 아직 연결되지 않았을 때 status가DOWN임을 확인합니다 — readiness의 예상된 동작과 정확히 일치합니다.
4.3. Deployment에서 readiness 선언
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
successThreshold: 1
successThreshold: 1: probe가 한 번만 성공하면 다시 ready로 간주합니다 (liveness와 달리, 기본값이 1이므로 보통 이 필드를 설정할 필요가 없습니다).periodSeconds: 5는 liveness보다 짧습니다. readiness는 dependency 상태의 변화에 빠르게 반응할 필요가 있기 때문입니다.
5. Startup probe
느린 시작 시간을 가진 애플리케이션의 경우 (큰 캐시 로드, DB 마이그레이션 실행), 고정된 initialDelaySeconds를 가진 liveness만 사용하면 충분히 큰 숫자를 추측해야 합니다. 잘못되기 쉽고 이후의 모든 재시작에 대해 대기 시간을 낭비합니다.
Startup probe는 이 문제를 해결합니다: startup probe가 아직 통과하지 못했을 때, Kubernetes는 liveness와 readiness probe를 임시로 비활성화합니다. 애플리케이션이 합법적으로 시작하는 동안 불필요하게 종료되지 않도록 합니다.
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 3
startupProbe는failureThreshold: 30과periodSeconds: 10을 사용합니다 → app이 최대 300초 동안 시작되도록 허용합니다 (30 × 10s) 실패로 간주되기 전에.startupProbe가 처음 통과한 후, Kubernetes는livenessProbe모니터링으로 전환합니다. 더 작은 threshold (3회 × 10초 = 30초)로 안정적으로 실행되는 동안 실제 문제에 빠르게 반응합니다.
주의: 애플리케이션이 빠르게 시작되면 (REST API 같은 간단한 애플리케이션처럼 몇 초 미만),
startupProbe를 선언할 필요가 없습니다. 추가하면 YAML만 길어지고 명확한 이점이 없습니다.
6. Deployment에서 세 가지를 모두 결합
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: nonladev/order-service:1.0
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
failureThreshold: 3
새로운 pod가 생성될 때의 작업 흐름:
- Container가 시작되면 Kubernetes는 즉시
startupProbe를 호출하기 시작합니다 (startupProbe가 대기를 처리하므로initialDelaySeconds가 없습니다). - 이 시점에
livenessProbe와readinessProbe는 아직 실행되지 않습니다. startupProbe가 처음으로 HTTP 200을 반환하면, Kubernetes는 startup probe를 완전히 중단합니다. liveness와 readiness를 동시에 실행하기 시작합니다.- Readiness 실패 → pod가 Service에서 제거됨, 트래픽이 더 이상 도착하지 않습니다. 하지만 container는 여전히 살아있습니다.
- Liveness 실패 → container가 종료됨, Kubernetes가 새 container를 생성합니다 (같은 pod에서), 프로세스는 1단계부터 다시 반복됩니다.
kubectl describe pod를 사용하여 실제 probe 상태를 확인합니다:
kubectl describe pod order-service-7d9f8c6b5-xk2pq
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Events:
Type Reason Age From Message
Warning Unhealthy 2m kubelet Readiness probe failed: HTTP probe failed with statuscode: 503
Readiness probe failed 줄은 readiness가 실패하고 있음을 나타냅니다. 애플리케이션 로그를 확인하거나 /actuator/health/readiness를 직접 호출하여 어떤 항목이 DOWN인지 확인하세요.
7. 흔한 실수들
- liveness와 readiness를 위해 동일한 endpoint를 사용합니다: 외부 dependency (DB, cache)가 죽을 때, 두 probe가 모두 실패하여 pod는 계속해서 재시작됩니다 (crash loop). 주요 애플리케이션은 여전히 건강함에도 불구하고 말입니다.
/actuator/health/liveness(내부만 확인)와/actuator/health/readiness(dependency도 확인)를 명확하게 분리하세요. initialDelaySeconds가 너무 작습니다: 시작이 느린 애플리케이션의 경우. 앱이 로드되는 중에 liveness probe가 즉시 실패하여 무한 restart loop를 유발합니다. 해결책: 임의로initialDelaySeconds를 늘리는 대신startupProbe를 사용하세요.periodSeconds×failureThreshold를 너무 크게 설정하는 것을 잊었습니다: pod에 실제 오류가 있지만 발견하는 데 몇 분이 걸리고, SLA에 영향을 줍니다.- Readiness probe가 중요하지 않은 downstream 서비스를 호출합니다: 예를 들어 로깅 서비스를 호출합니다. 그 서비스가 다운되면 전체
OrderService가 불필요하게 not-ready로 간주됩니다. 주요 기능이 여전히 잘 작동하더라도 말입니다.
8. 결론
세 가지 probe 유형은 세 가지 명확하게 분리된 목적을 제공합니다: startupProbe는 느린 시작 단계에서 애플리케이션을 보호하고, livenessProbe는 deadlock을 감지하여 Kubernetes가 자동으로 재시작하도록 하며, readinessProbe는 실제 dependency 상태에 따라 트래픽을 조정합니다. 가장 중요한 원칙은 liveness probe를 외부 시스템에 의존하지 않도록 하는 것입니다 — 그렇게 하면 downstream 문제가 자신의 서비스의 crash loop로 변환됩니다. 세 probe를 올바르게 구성하면 cluster가 자동으로 복구되어 배포 또는 인프라 문제 발생 시 downtime을 크게 줄일 수 있습니다.