본문 바로가기
클라우드/쿠버네티스

Pod가 갑자기 사라진다고? 🚨 Kubernetes Graceful Shutdown 완전 정복

by gasbugs 2026. 4. 3.
반응형

Scale-in 할 때마다 클라이언트가 에러를 받는다면,
그건 설계의 문제가 아니라 종료 전략의 문제다.

종료 직전 모든 작업을 완료하고 안전하게 마무리하는 Graceful Shutdown의 서사 — 혼돈 속에서도 질서 있게 정리하고 떠나는 선원의 마음.

🎯 이 글에서 다루는 것

  • Pod가 종료될 때 실행 중인 요청에 무슨 일이 생기는지
  • SIGTERM과 Graceful Shutdown의 동작 원리
  • terminationGracePeriodSeconds 설정으로 종료를 안전하게 만드는 방법
  • preStop Hook 활용법
  • MSA 환경에서 권장하는 타임아웃 전략

📌 도입 / 배경

Kubernetes의 HPA(Horizontal Pod Autoscaler)는 트래픽이 줄면 Pod 수를 자동으로 줄여준다. 이건 아주 훌륭한 기능이다.

근데 여기서 현실적인 문제가 생긴다.

"Pod가 줄어드는 순간, 그 Pod에서 처리 중이던 요청은 어떻게 되는 걸까?"

 

아무 처리를 하지 않으면 아래처럼 된다.

  1. HPA가 Pod를 종료하기로 결정
  2. Pod 안에서 API 요청을 열심히 처리하는 중
  3. Pod가 강제로 SIGKILL을 받고 즉시 사라짐
  4. 클라이언트는 응답도 못 받고 TCP 연결이 끊김
  5. 클라이언트는 timeout까지 기다리다가 Connection reset 또는 502/504 에러 수신

이 글에서는 이 문제를 Graceful Shutdown(우아한 종료) 전략으로 해결하는 방법을 다룬다.


🔍 Kubernetes Pod 종료 흐름 이해하기

Pod가 삭제될 때 Kubernetes는 아래 순서로 동작한다.

1. kubectl delete pod / HPA scale-in 명령
2. Pod 상태 → Terminating
3. Endpoint에서 해당 Pod IP 제거 (더 이상 새 요청을 받지 않음)
4. Container에 SIGTERM 신호 전송
5. terminationGracePeriodSeconds 동안 대기 (기본 30초)
6. 기간 내 종료 안 되면 SIGKILL로 강제 종료

 

여기서 핵심은 3번과 4번이 동시에 발생하지 않는다는 점이다.

 

kube-proxy와 Ingress Controller가 Endpoint 변경을 감지하는 데 수 초의 전파 지연(propagation delay) 이 있다. 즉, SIGTERM을 받은 순간에도 새 요청이 들어올 수 있다는 뜻이다.


💡 해결책 1 — SIGTERM 핸들링 + terminationGracePeriodSeconds

가장 근본적인 해결책이다. 애플리케이션이 SIGTERM을 받으면 즉시 죽지 말고, 처리 중인 요청을 마저 끝낸 뒤 종료하도록 만든다.

애플리케이션 레벨 처리 (Node.js 예시)

const server = app.listen(3000);

process.on('SIGTERM', () => {
  console.log('SIGTERM received. Closing server gracefully...');
  
  // 새 연결은 받지 않고, 기존 연결 처리 완료 대기
  server.close(() => {
    console.log('All connections closed. Exiting.');
    process.exit(0);
  });
  
  // 안전장치: 25초 후 강제 종료 (gracePeriod보다 짧게)
  setTimeout(() => {
    console.error('Forced exit after timeout');
    process.exit(1);
  }, 25000);
});

Java Spring Boot 예시

Spring Boot 2.3 이상에서는 설정 한 줄로 Graceful Shutdown을 활성화할 수 있다.

# application.yaml
server:
  shutdown: graceful   # 기본값은 immediate

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Kubernetes Deployment 설정

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
spec:
  template:
    spec:
      # SIGKILL까지의 유예 시간 (기본 30초)
      terminationGracePeriodSeconds: 60
      containers:
        - name: my-api
          image: my-api:latest
          lifecycle:
            preStop:
              exec:
                # Endpoint 전파 지연을 커버하기 위한 sleep
                command: ["/bin/sh", "-c", "sleep 5"]

💡 preStop hook의 역할: SIGTERM이 전송되기 직전에 실행된다. sleep 5를 주면 kube-proxy가 Endpoint를 제거하는 동안 새 요청이 들어오는 것을 막아준다. 사소해 보이지만 현업에서 매우 효과적인 패턴이다.


💡 해결책 2 — MSA 표준 패턴: Retry + 짧은 Timeout

MSA(마이크로서비스 아키텍처) 환경에서 권장하는 또 다른 접근법이다.

 

서버를 완벽하게 만드는 것도 중요하지만, 클라이언트도 일시적 장애에 스스로 대응할 수 있도록 설계해야 한다.

핵심 원칙은 이렇다:

  • Timeout을 짧게: 오래 기다리지 않고 빠르게 실패를 감지
  • Retry with Backoff: 실패 시 일정 간격으로 재시도
  • Circuit Breaker: 연속 실패 시 요청 차단 (Resilience4j, Istio 등)
// Resilience4j Retry 설정 예시
RetryConfig config = RetryConfig.custom()
  .maxAttempts(3)
  .waitDuration(Duration.ofMillis(500))
  .retryOnException(e -> e instanceof ConnectException
                       || e instanceof SocketTimeoutException)
  .build();

 

이 방식의 장점은 Pod 종료 외에도 네트워크 일시 장애, 재배포, 노드 장애 등 다양한 상황에 대응할 수 있다는 점이다.


💻 실전 권장 조합

두 가지를 조합하면 가장 안정적인 구성이 된다.

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60   # 서버 측 유예 시간
      containers:
        - name: my-api
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]   # Endpoint 전파 대기
          # Readiness Probe로 트래픽 수신 제어
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5

 

그리고 애플리케이션에서는:

  1. /health/ready 엔드포인트를 만들어 SIGTERM 수신 시 503 반환
  2. 기존 처리 중인 요청은 계속 처리
  3. 모든 요청 완료 후 프로세스 종료

이렇게 하면 readinessProbe가 실패kube-proxy가 Endpoint 제거새 요청 유입 차단기존 요청 처리 완료Pod 종료의 깔끔한 흐름이 만들어진다.


⚠️ 주의사항 / 흔한 실수

🔴 terminationGracePeriodSeconds를 너무 길게 설정

600초(10분)처럼 길게 설정하면 배포 시간이 극단적으로 늘어난다. 일반적으로 60~120초가 적절하다.

 

🔴 preStop sleep 없이 SIGTERM만 믿기

kube-proxy의 Endpoint 전파 지연 때문에 SIGTERM 직후에도 새 요청이 들어온다. preStop: sleep 5 정도는 반드시 넣어야 한다.

 

🔴 SIGTERM 핸들러 없는 언어/프레임워크 주의

일부 오래된 프레임워크나 쉘 스크립트로 실행되는 컨테이너는 SIGTERM을 기본적으로 무시한다. exec 명령으로 프로세스를 PID 1로 실행해야 SIGTERM이 전달된다.

# 잘못된 예 — SIGTERM이 sh로 가서 앱에 전달 안 됨
CMD ["sh", "-c", "node server.js"]

# 올바른 예 — node가 PID 1이 되어 SIGTERM 직접 수신
CMD ["node", "server.js"]

 

🔴 Long-polling / WebSocket은 별도 처리 필요

HTTP 요청과 달리 지속 연결은 grace period 내에 자연 종료가 안 될 수 있다. 해당 연결 유형은 SIGTERM 수신 시 명시적으로 연결을 닫는 로직이 필요하다.


✅ 정리 / 마무리

Kubernetes에서 Pod가 종료될 때 진행 중인 요청이 유실되는 문제는 설계 결함이 아니라 설정의 문제다. 아래 세 가지를 적용하면 대부분의 상황을 커버할 수 있다.

레이어 설정 목적
Kubernetes terminationGracePeriodSeconds: 60 SIGKILL 유예 시간 확보
Kubernetes preStop: sleep 5 Endpoint 전파 지연 대기
애플리케이션 SIGTERM 핸들러 구현 기존 요청 완료 후 종료
클라이언트 Retry + 짧은 Timeout 일시 장애 자동 복구

 

이 네 가지를 모두 갖춘 시스템이라면 Scale-in 과정에서 클라이언트가 에러를 받는 상황은 사실상 발생하지 않는다.

 

다음 단계로는 Istio나 Linkerd 같은 Service Mesh를 도입하면 이 모든 패턴을 애플리케이션 코드 변경 없이 인프라 레벨에서 자동으로 처리할 수 있다.

 

반응형