2P by GN⁺ 8시간전 | ★ favorite | 댓글 1개
  • 우아한 종료(graceful shutdown) 는 애플리케이션이 종료 신호를 받은 뒤 새 요청을 막고, 현재 요청을 마치고, 리소스를 정리하는 절차로 구성
  • Go에서는 os/signal 패키지를 사용해 SIGINT, SIGTERM 같은 종료 신호를 직접 처리할 수 있으며, signal.NotifyContext를 이용해 context 기반 종료 제어도 가능함
  • HTTP 서버 종료 시에는 Server.Shutdown() 호출 전 readiness probe 실패를 통해 트래픽을 차단하고, 몇 초 대기한 뒤 shutdown을 수행하는 것이 안정적임
  • 모든 핸들러는 context 종료 신호를 감지하고 종료 가능해야 하며, BaseContext 또는 middleware를 통해 이를 통합적으로 처리할 수 있음
  • 종료 신호 수신 후 데이터베이스, 메시지 브로커, 캐시 등 외부 리소스는 의도적으로 정리해야 하며, defer로 등록하면 종료 순서 관리가 쉬움

Graceful Shutdown이란?

  • 우아한 종료는 애플리케이션이 종료될 때 새로운 요청 차단, 진행 중 요청 완료 대기, 리소스 정리를 거치는 프로세스임
  • 이 글은 주로 HTTP 서버와 컨테이너 환경을 다루지만, 모든 애플리케이션에 적용 가능한 개념

1. 종료 신호 처리

  • Unix 계열 시스템에서는 SIGTERM, SIGINT, SIGHUP 등이 종료 신호로 사용됨
  • Go 런타임은 SIGTERM, SIGINT 수신 시 기본적으로 애플리케이션을 종료하지만, os/signal.Notify로 직접 처리 가능함
  • 버퍼링된 채널(용량 1) 을 사용하면 초기화 중 신호 유실 방지 가능
  • Go 1.16 이후에는 signal.NotifyContext를 사용해 context 기반 신호 제어가 간편해짐

2. 종료 시간 인식

  • Kubernetes에서는 기본적으로 30초의 종료 유예 기간이 주어짐 (terminationGracePeriodSeconds)
  • 안전하게 종료하려면 20% 여유를 두고 25초 이내에 종료 작업을 완료하는 것이 바람직함

3. 새 요청 받기 중단

  • http.Server.Shutdown()새 연결을 차단하고 기존 요청이 완료될 때까지 대기
  • Kubernetes 환경에서는 readiness probe를 먼저 실패하게 만들어 트래픽 유입을 차단한 뒤 약간 대기 후 shutdown 수행
  • readiness 핸들러에서는 전역 변수로 종료 상태를 판단하여 HTTP 503 반환하도록 구성 가능함

4. 요청 처리 마무리

  • 종료를 위한 context에 적절한 timeout 설정 필요 (context.WithTimeout)
  • shutdown context가 만료되면 남은 연결은 강제 종료
  • 모든 핸들러는 context.Context를 활용해 종료 신호를 감지하고 중단 가능하도록 설계해야 함
  • 이를 위해 middleware나 BaseContext를 통해 모든 요청에 종료 context를 주입할 수 있음

5. 리소스 정리

  • 종료 신호를 받았다고 바로 리소스를 닫으면 처리 중인 핸들러에 문제 발생 가능
  • shutdown이 완료된 뒤에 데이터베이스 연결, 메시지 브로커, 캐시 등을 정리해야 함
  • Go의 defer를 활용하면 초기화 역순으로 종료 루틴 실행 가능하여 의존성 관리가 쉬움
  • 메모리, 파일 디스크립터 등 OS가 자동으로 정리하는 리소스 외에도 데이터 flush, 트랜잭션 rollback 등 명시적 종료가 필요한 리소스 존재

전체 예제 요약

  • signal.NotifyContext로 종료 신호 수신
  • /healthz readiness 엔드포인트 구현
  • BaseContext로 모든 요청에 종료 context 주입
  • readiness 5초 대기 후 shutdown 수행
  • server.Shutdown 호출 실패 시 강제 종료 fallback 포함

참고 문헌 및 관련 리소스

Hacker News 의견
  • Kubernetes에서 로드밸런서 타겟 IP 업데이트가 오래 걸리는 경우가 있음. 90%의 문제는 트래픽이 실제로 드레인되는지 확인하는 것임

    • 글로벌 preStop 훅에 15초 대기 시간을 추가하여 HTTP 503 비율을 크게 개선함
    • 로드밸런서 등록 취소와 SIGTERM 전달 사이에 시간을 만들어 애플리케이션 처리 간소화함
  • log.Fatal 사용 시 defer 안의 내용이 실행되지 않음

    • log.Fatalos.Exit를 호출하여 즉시 종료함
    • panic을 사용하면 defer 내용이 실행됨
  • Prometheus /metrics 엔드포인트가 주기적으로 스크랩될 때, 마지막 스크랩과 프로세스 종료 사이에 기록된 메트릭이 전파되지 않을 수 있음

    • 서비스 종료 시 마지막 몇 초의 로그를 잃을 수 있음
    • 로그 파일이 사이드카 프로세스에 의해 감시될 때 경합 조건이 발생할 수 있음
  • 분산 시스템이 클라이언트의 정상 종료에 의존하면 시스템이 심각하게 고장날 수 있음

  • 새로운 서비스 인스턴스가 이전 인스턴스로부터 소켓을 받을 때 연결을 끊지 않고 애플리케이션을 재시작하는 방법에 대한 설명이 부족함

    • systemd에서는 구현이 비교적 간단함
    • nginx는 20년 넘게 이를 지원함
    • Kubernetes와 Docker는 이를 지원하지 않음
  • liveness에 대한 논의가 부족함

    • 동일한 엔드포인트를 liveness/readiness에 사용하는 앱이 여러 번 보였음
  • 프로그램이 ctrl c와 같은 명령을 깨끗하게 처리하지 못하면 잘못 작성된 것임

  • Elixir는 프로세스를 작은 VM 프로세스로 설계하여 정상 종료 루틴을 의도적으로 만들 필요가 없게 함

  • 프로젝트에서 정상 종료를 처리하기 위한 작은 라이브러리를 만듦

    • 다양한 시작 및 종료 메커니즘을 가진 서비스를 통합하기 위한 API를 제공함
  • readiness probe 업데이트 후 몇 초 기다려 시스템이 새로운 요청을 보내지 않도록 함

    • 종료 중인 pod는 준비 상태가 아님
    • 서비스는 엔드포인트를 종료 중으로 표시함
    • SIGTERM 후에도 작은 창이 있을 수 있지만, 이는 큰 문제가 아님
    • 새로운 연결을 받지 않고 기존 연결을 정상적으로 종료하는 것이 중요함