Kubernetes Probes: Liveness

Kubernetes Probes란? 컨테이너 헬스 체크의 핵심

Kubernetes에서 Pod가 Running 상태라고 해서 실제로 트래픽을 처리할 수 있는 것은 아닙니다. “애플리케이션이 데드락에 빠져 응답을 못 한다”, “DB 커넥션 풀이 고갈되어 요청을 처리할 수 없다”, “JVM이 아직 워밍업 중이라 준비가 안 됐다” — 이런 상황을 자동으로 감지하고 대응하는 것이 Probe(프로브)입니다.

Kubernetes는 3가지 Probe를 제공합니다: Liveness(생존), Readiness(준비), Startup(시작). 각각의 목적이 다르고, 잘못 설정하면 오히려 장애를 악화시킵니다. 이 글에서는 3가지 Probe의 동작 원리와 차이, HTTP/TCP/gRPC/Exec 핸들러 선택, 파라미터 튜닝 전략, 그리고 Resource Management와 결합한 안정적 운영 패턴까지 완전히 다룹니다.

3가지 Probe 비교: Liveness vs Readiness vs Startup

Probe 질문 실패 시 동작 주요 목적
Liveness 살아 있는가? 컨테이너 재시작 데드락, 무한루프 감지
Readiness 트래픽 받을 준비가 됐는가? Service 엔드포인트에서 제거 (트래픽 차단) 초기화 대기, 의존성 장애
Startup 시작이 완료됐는가? 컨테이너 재시작 (failureThreshold 초과 시) 느린 시작 앱 보호 (JVM 등)

Probe 실행 타임라인

컨테이너 시작
│
├─ Startup Probe 시작 (설정된 경우)
│   ├─ 성공할 때까지 Liveness/Readiness는 비활성
│   ├─ 성공 → Startup Probe 종료, Liveness/Readiness 시작
│   └─ failureThreshold 초과 → 컨테이너 재시작
│
├─ Liveness Probe 시작
│   ├─ 성공 → 정상
│   └─ failureThreshold 초과 → 컨테이너 재시작 (restartPolicy에 따라)
│
└─ Readiness Probe 시작
    ├─ 성공 → Service 엔드포인트에 추가 (트래픽 수신)
    └─ 실패 → Service 엔드포인트에서 제거 (트래픽 차단)

핵심 차이: Liveness 실패는 컨테이너를 죽이고 재시작합니다. Readiness 실패는 컨테이너를 죽이지 않고 트래픽만 차단합니다. 이 차이를 혼동하면 심각한 운영 사고가 발생합니다.

Probe 핸들러 4가지: HTTP, TCP, gRPC, Exec

1. HTTP GET (가장 일반적)

livenessProbe:
  httpGet:
    path: /healthz         # 헬스 체크 경로
    port: 8080
    httpHeaders:            # 커스텀 헤더 (선택)
    - name: X-Custom-Header
      value: probe
  initialDelaySeconds: 10   # 첫 체크까지 대기
  periodSeconds: 15         # 체크 간격
  timeoutSeconds: 3         # 응답 대기 시간
  successThreshold: 1       # 성공 판정 횟수
  failureThreshold: 3       # 실패 허용 횟수

# HTTP 200~399 → 성공
# 그 외 (400, 500 등) → 실패

2. TCP Socket

# 포트 연결 가능 여부만 확인 (DB, Redis 등)
livenessProbe:
  tcpSocket:
    port: 5432
  periodSeconds: 10
  failureThreshold: 3

3. gRPC (Kubernetes 1.27+ GA)

# gRPC Health Checking Protocol 사용
livenessProbe:
  grpc:
    port: 50051
    service: "myapp"      # 서비스 이름 (선택)
  periodSeconds: 10

4. Exec (커맨드 실행)

# 커맨드 exit code 0 → 성공, 그 외 → 실패
livenessProbe:
  exec:
    command:
    - /bin/sh
    - -c
    - "pg_isready -U postgres"
  periodSeconds: 10

# 또는 파일 존재 여부 확인
livenessProbe:
  exec:
    command:
    - cat
    - /tmp/healthy

핸들러 선택 기준

핸들러 적합한 경우 주의사항
HTTP GET 웹 서비스, REST API 엔드포인트가 가벼워야 함
TCP Socket DB, 캐시, 메시지 큐 포트 열림 ≠ 서비스 정상
gRPC gRPC 서비스 Health Protocol 구현 필요
Exec 커스텀 체크, 레거시 앱 프로세스 fork 오버헤드

파라미터 상세 설명과 튜닝

파라미터 기본값 설명 튜닝 기준
initialDelaySeconds 0 첫 Probe까지 대기 시간 앱 시작 시간 기반 (Startup Probe 사용 시 0)
periodSeconds 10 체크 간격 Liveness: 10~30s / Readiness: 5~10s
timeoutSeconds 1 응답 대기 시간 엔드포인트 응답 시간 + 여유
successThreshold 1 성공 판정 최소 횟수 Liveness: 항상 1 / Readiness: 1~3
failureThreshold 3 실패 허용 최대 횟수 Liveness: 3~5 / Startup: 시작시간/period

Liveness 감지 시간 계산:

# 장애 감지 → 재시작까지 최대 시간
maxDetectionTime = initialDelaySeconds + (periodSeconds × failureThreshold)

# 예: initialDelay=0, period=10, failure=3
# → 최대 30초 후 재시작

# 예: initialDelay=0, period=15, failure=5
# → 최대 75초 후 재시작

Startup Probe: 느린 시작 애플리케이션 보호

Spring Boot, JVM 기반 앱은 시작에 30초~2분이 걸릴 수 있습니다. Startup Probe 없이 Liveness만 사용하면 시작도 완료되기 전에 Liveness가 실패하여 무한 재시작 루프에 빠집니다:

# ❌ 잘못된 설정: 시작에 60초 걸리는 앱
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10   # 10초 후 첫 체크
  periodSeconds: 10
  failureThreshold: 3
  # 10 + (10 × 3) = 40초 후 재시작 → 앱이 60초 걸리므로 무한 재시작!

# ✅ Startup Probe로 시작 시간 보호
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 5
  failureThreshold: 30       # 5 × 30 = 150초까지 시작 허용
  # Startup 성공 전까지 Liveness/Readiness 비활성

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 15
  failureThreshold: 3
  # Startup 성공 후에만 시작 → initialDelaySeconds 불필요!

핵심: Startup Probe가 있으면 initialDelaySeconds를 0으로 설정해도 됩니다. Startup Probe가 시작 완료를 보장하므로, Liveness는 운영 중 장애만 감지하면 됩니다.

실무 패턴 1: NestJS/Express 헬스 엔드포인트 설계

// NestJS: @nestjs/terminus 활용
@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: MikroOrmHealthIndicator,
    private redis: RedisHealthIndicator,
  ) {}

  // Liveness: 프로세스 살아있는지만 확인 (가볍게!)
  @Get('live')
  @HealthCheck()
  liveness() {
    return this.health.check([]);  // 아무 체크 없이 200 반환
  }

  // Readiness: 의존성까지 확인 (트래픽 받을 준비)
  @Get('ready')
  @HealthCheck()
  readiness() {
    return this.health.check([
      () => this.db.pingCheck('database', { timeout: 1500 }),
      () => this.redis.pingCheck('redis', { timeout: 1000 }),
    ]);
  }

  // Startup: 초기화 완료 여부
  @Get('startup')
  @HealthCheck()
  startup() {
    return this.health.check([
      () => this.db.pingCheck('database', { timeout: 3000 }),
    ]);
  }
}
# Pod 스펙
containers:
- name: app
  image: myapp:latest
  ports:
  - containerPort: 3000
  startupProbe:
    httpGet:
      path: /health/startup
      port: 3000
    periodSeconds: 5
    failureThreshold: 24     # 최대 120초 시작 대기
  livenessProbe:
    httpGet:
      path: /health/live
      port: 3000
    periodSeconds: 15
    failureThreshold: 3
    timeoutSeconds: 3
  readinessProbe:
    httpGet:
      path: /health/ready
      port: 3000
    periodSeconds: 5
    failureThreshold: 3
    timeoutSeconds: 3

실무 패턴 2: Spring Boot Actuator 연동

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health
  endpoint:
    health:
      probes:
        enabled: true         # /actuator/health/liveness, /readiness 활성화
      group:
        liveness:
          include: livenessState
        readiness:
          include: readinessState,db,redis
  health:
    db:
      enabled: true
    redis:
      enabled: true
# K8s Probe 설정
startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 5
  failureThreshold: 36     # 최대 180초 (JVM + Spring 초기화)
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 15
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  periodSeconds: 5
  failureThreshold: 3

실무 패턴 3: Graceful Shutdown과 Probe 연동

# Pod 종료 시 흐름:
# 1. SIGTERM 수신
# 2. Readiness Probe 실패 → Service 엔드포인트에서 제거
# 3. 진행 중인 요청 처리 완료 (terminationGracePeriodSeconds 내)
# 4. SIGKILL (강제 종료)

spec:
  terminationGracePeriodSeconds: 60  # 기본 30초
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]
          # Service 엔드포인트 제거가 전파될 시간 확보
    readinessProbe:
      httpGet:
        path: /health/ready
        port: 3000
      periodSeconds: 5

preStop + Readiness 조합이 중요한 이유: SIGTERM을 받은 즉시 Readiness를 실패시키면 좋지만, Service 엔드포인트 업데이트가 모든 노드에 전파되기까지 시간이 걸립니다. preStop: sleep 5로 전파 시간을 확보해야 노드 드레인 시 5xx를 방지할 수 있습니다.

흔한 실수와 안티패턴 6가지

1. Liveness에 외부 의존성 체크 포함

# ❌ DB가 다운되면 모든 Pod가 재시작 → 연쇄 장애!
livenessProbe:
  httpGet:
    path: /health       # DB + Redis + 외부 API 모두 체크
    port: 8080

# ✅ Liveness는 프로세스 자체만 확인
livenessProbe:
  httpGet:
    path: /health/live  # 단순 200 응답만
    port: 8080

# 외부 의존성은 Readiness에서 확인
readinessProbe:
  httpGet:
    path: /health/ready # DB + Redis 체크
    port: 8080

이것은 가장 치명적인 실수입니다. DB 장애 시 Liveness가 실패하면 모든 Pod가 재시작되고, 재시작된 Pod도 DB에 연결 못 해서 다시 재시작 → Cascading Failure(연쇄 장애)가 발생합니다.

2. 헬스 엔드포인트가 무거운 작업 수행

// ❌ 헬스 체크에서 풀 테이블 스캔
@Get('health')
async health() {
  const count = await this.userRepo.count();  // 대형 테이블!
  return { status: 'ok', users: count };
}

// ✅ 가벼운 쿼리만
@Get('health/ready')
async ready() {
  await this.em.execute('SELECT 1');  // 단순 ping
  return { status: 'ok' };
}

3. timeoutSeconds가 너무 짧음

# ❌ GC pause나 일시적 부하로 1초 초과 시 실패
livenessProbe:
  timeoutSeconds: 1     # 기본값이지만 너무 짧을 수 있음

# ✅ 여유를 두고 설정
livenessProbe:
  timeoutSeconds: 3     # JVM GC pause 고려

4. Startup Probe 없이 initialDelaySeconds만 사용

# ❌ 시작 시간이 가변적이면 initialDelay로 커버 불가
livenessProbe:
  initialDelaySeconds: 120   # 항상 120초 대기 → 비효율

# ✅ Startup Probe로 유연하게
startupProbe:
  periodSeconds: 5
  failureThreshold: 30       # 최대 150초, 빨리 시작하면 빨리 통과
livenessProbe:
  initialDelaySeconds: 0     # Startup 통과 후 즉시 시작

5. Readiness 없이 Liveness만 사용

# ❌ 앱이 시작 중인데 트래픽이 들어옴 → 5xx 에러
containers:
- name: app
  livenessProbe: { ... }
  # readinessProbe 없음!

# ✅ Readiness로 준비 완료 전 트래픽 차단
containers:
- name: app
  livenessProbe: { ... }
  readinessProbe: { ... }   # 반드시 함께 설정!

6. failureThreshold: 1 설정

# ❌ 네트워크 일시 장애 한 번에 컨테이너 재시작
livenessProbe:
  failureThreshold: 1    # 한 번 실패 = 재시작

# ✅ 일시적 장애 허용
livenessProbe:
  failureThreshold: 3    # 3번 연속 실패 시에만 재시작

워크로드별 Probe 설정 가이드

워크로드 시작 시간 Startup Liveness Readiness
NestJS/Express ~5s 선택 HTTP /health/live HTTP /health/ready (DB체크)
Spring Boot 30~120s 필수 HTTP /actuator/health/liveness HTTP /actuator/health/readiness
PostgreSQL ~10s 선택 Exec pg_isready Exec pg_isready
Redis ~2s 불필요 TCP 6379 Exec redis-cli ping
gRPC 서비스 가변 권장 gRPC health gRPC health

Probe 디버깅: 문제 진단 방법

# 1. Pod 이벤트에서 Probe 실패 확인
kubectl describe pod myapp-xyz
# Events:
#   Warning  Unhealthy  Liveness probe failed: HTTP probe failed with statuscode: 503
#   Warning  Unhealthy  Readiness probe failed: connection refused
#   Normal   Killing    Container myapp failed liveness probe, will be restarted

# 2. Pod 재시작 횟수 확인
kubectl get pods -o wide
# NAME         READY   STATUS    RESTARTS   AGE
# myapp-xyz    0/1     Running   5          10m    ← 재시작 5번 = Probe 문제

# 3. 컨테이너 안에서 직접 헬스 체크 테스트
kubectl exec myapp-xyz -- wget -qO- http://localhost:3000/health/live
kubectl exec myapp-xyz -- wget -qO- http://localhost:3000/health/ready

# 4. 이전 컨테이너 로그 확인 (재시작 전)
kubectl logs myapp-xyz --previous

정리: Kubernetes Probes 설계 체크리스트

항목 체크
Liveness에 외부 의존성 체크 미포함 (연쇄 장애 방지)
Readiness에 DB/Redis 등 의존성 체크 포함
느린 시작 앱(JVM 등)은 Startup Probe 필수
헬스 엔드포인트는 가벼운 체크만 (SELECT 1 수준)
failureThreshold ≥ 3 (일시적 장애 허용)
timeoutSeconds ≥ 2~3 (GC pause 고려)
Liveness/Readiness 엔드포인트 분리 (/live vs /ready)
preStop + terminationGracePeriodSeconds로 graceful shutdown
장애 감지 시간 계산: period × failureThreshold
Probe 실패 시 describe/logs로 원인 진단

Kubernetes Probes는 “장애를 감지하고 자동 복구하는” 클러스터의 자가 치유 메커니즘입니다. 핵심은 Liveness는 가볍게(프로세스 생존만), Readiness는 꼼꼼하게(의존성 포함), Startup은 넉넉하게(시작 시간 보호) 설정하는 것입니다. 특히 Liveness에 외부 의존성을 포함하는 실수는 장애를 치유하는 것이 아니라 오히려 연쇄 장애를 일으키므로, 반드시 Liveness와 Readiness의 역할을 분리해야 합니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux