Go 언어에서 Graceful Shutdown을 구현하는 실용적 패턴
(victoriametrics.com)- 우아한 종료(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 포함
참고 문헌 및 관련 리소스
- Go로 작성된 기타 인프라 가이드
- Graceful Shutdown 외에도 defer 활용법, Go 배열/슬라이스/맵 동작 원리 등도 제공됨
Hacker News 의견
-
Kubernetes에서 로드밸런서 타겟 IP 업데이트가 오래 걸리는 경우가 있음. 90%의 문제는 트래픽이 실제로 드레인되는지 확인하는 것임
- 글로벌 preStop 훅에 15초 대기 시간을 추가하여 HTTP 503 비율을 크게 개선함
- 로드밸런서 등록 취소와 SIGTERM 전달 사이에 시간을 만들어 애플리케이션 처리 간소화함
-
log.Fatal
사용 시defer
안의 내용이 실행되지 않음-
log.Fatal
은os.Exit
를 호출하여 즉시 종료함 -
panic
을 사용하면defer
내용이 실행됨
-
-
Prometheus
/metrics
엔드포인트가 주기적으로 스크랩될 때, 마지막 스크랩과 프로세스 종료 사이에 기록된 메트릭이 전파되지 않을 수 있음- 서비스 종료 시 마지막 몇 초의 로그를 잃을 수 있음
- 로그 파일이 사이드카 프로세스에 의해 감시될 때 경합 조건이 발생할 수 있음
-
분산 시스템이 클라이언트의 정상 종료에 의존하면 시스템이 심각하게 고장날 수 있음
-
새로운 서비스 인스턴스가 이전 인스턴스로부터 소켓을 받을 때 연결을 끊지 않고 애플리케이션을 재시작하는 방법에 대한 설명이 부족함
- systemd에서는 구현이 비교적 간단함
- nginx는 20년 넘게 이를 지원함
- Kubernetes와 Docker는 이를 지원하지 않음
-
liveness에 대한 논의가 부족함
- 동일한 엔드포인트를 liveness/readiness에 사용하는 앱이 여러 번 보였음
-
프로그램이 ctrl c와 같은 명령을 깨끗하게 처리하지 못하면 잘못 작성된 것임
-
Elixir는 프로세스를 작은 VM 프로세스로 설계하여 정상 종료 루틴을 의도적으로 만들 필요가 없게 함
-
프로젝트에서 정상 종료를 처리하기 위한 작은 라이브러리를 만듦
- 다양한 시작 및 종료 메커니즘을 가진 서비스를 통합하기 위한 API를 제공함
-
readiness probe 업데이트 후 몇 초 기다려 시스템이 새로운 요청을 보내지 않도록 함
- 종료 중인 pod는 준비 상태가 아님
- 서비스는 엔드포인트를 종료 중으로 표시함
- SIGTERM 후에도 작은 창이 있을 수 있지만, 이는 큰 문제가 아님
- 새로운 연결을 받지 않고 기존 연결을 정상적으로 종료하는 것이 중요함