- Go 애플리케이션의 Graceful Shutdown은 새 요청을 막고, 진행 중인 작업을 기다린 뒤, 데이터베이스 연결·파일 락·네트워크 리스너 같은 자원을 정리하는 종료 절차임
- 종료 처리는
SIGTERM·SIGINT같은 종료 시그널을os/signal또는 Go 1.16 이상의signal.NotifyContext로 받아 기본 즉시 종료 동작을 대체하는 데서 시작함 - Kubernetes에서는 기본 30초 grace period 안에 종료를 마쳐야 하며,
preStop지연이나 readiness probe 실패로 외부 로드밸런서까지 트래픽 중단 상태가 전파될 시간을 확보해야 함 http.Server.Shutdown은 새 연결을 막고 활성 요청 완료를 기다리지만, 핸들러가 context cancellation을 따르지 않으면 부분 쓰기, 데이터 손실, 열린 트랜잭션 같은 문제가 생길 수 있음- 중요 자원은 종료 시그널 직후가 아니라 요청 종료 이후나 제한 시간 만료 뒤에 정리해야 하며, 초기화의 역순으로 종료하면 컴포넌트 의존성을 지키기 쉬움
Graceful Shutdown의 최소 조건
- Graceful Shutdown은 보통 세 가지 조건을 만족해야 함
- HTTP, pub/sub 같은 진입점에서 새 요청이나 메시지를 더 받지 않음
- 이미 진행 중인 요청이 끝날 때까지 기다리고, 너무 오래 걸리면 graceful error로 응답함
- 데이터베이스 연결, 파일 락, 네트워크 리스너 같은 중요 자원을 해제하고 마지막 정리를 수행함
- 외부 서비스로 나가는 데이터베이스나 캐시 연결은 새 요청 차단 단계에서 바로 끊지 않음
- 초점은 HTTP 서버와 컨테이너 애플리케이션이지만, 핵심 원리는 다른 애플리케이션에도 적용 가능함
종료 시그널 처리
- Unix 계열 시스템에서 시그널은 프로세스에 특정 상황이 발생했음을 알리는 소프트웨어 인터럽트임
- 프로세스는 특정 시그널에 핸들러를 등록할 수 있고, 핸들러가 없으면 기본 동작을 따름
- 기본 동작은 종료, 정지, 계속 실행, 무시 등이 될 수 있음
SIGKILL같은 일부 시그널은 잡거나 무시할 수 없으며 프로세스를 종료함
- Go 런타임은
main함수 실행 전부터SIGTERM,SIGQUIT,SIGILL,SIGTRAP등 여러 시그널 핸들러를 자동 등록함 - Graceful Shutdown에서 주로 중요한 종료 시그널은 세 가지임
SIGTERM: 프로세스에 종료를 요청하는 표준적이고 완곡한 방식이며, Kubernetes가 강제 종료 전에 애플리케이션에 보내는 시그널임SIGINT: 사용자가 터미널에서Ctrl+C로 프로세스를 멈추려 할 때 전송됨SIGHUP: 원래 터미널 연결 해제에 쓰였고, 현재는 설정 재로드 신호로도 자주 활용됨
- 별도 처리 없이
SIGTERM,SIGINT,SIGHUP을 받으면 Go 런타임은 애플리케이션을 종료함
os/signal과 NotifyContext
signal.Notify는 지정한 시그널을 기본 동작 대신 채널로 전달하도록 Go 런타임에 지시함- 시그널 채널은 버퍼 크기 1로 만드는 편이 안정적임
- Go 내부는 채널 전송에
select와default를 사용함 - 버퍼에 공간이 있으면 시그널이 전달되고, 버퍼가 가득 차면 시그널은 버려짐
- 버퍼 없는 채널에서 수신 중인 goroutine이 없으면 시그널을 놓칠 수 있음
- Go 내부는 채널 전송에
signal.Notify는 같은 시그널에 대해 여러 번 호출할 수 있으며, Go는 등록된 모든 채널에 해당 시그널을 보냄Ctrl+C를 여러 번 눌러도 보통 두 번째 입력이 자동으로SIGKILL로 승격되지는 않음- 대부분의 bash나 Linux 셸은 자동 승격을 하지 않음
- 강제 종료는
kill -9로SIGKILL을 직접 보내야 함
- 로컬 개발에서 두 번째
Ctrl+C로 강제 종료되게 하려면 첫 번째 시그널을 받은 직후signal.Stop으로 추가 시그널 수신을 중단할 수 있음 - Go 1.16부터는
signal.NotifyContext로 시그널 처리를 context cancellation과 연결할 수 있음ctx.Done()이후에도stop()을 호출해야 두 번째Ctrl+C가 애플리케이션을 강제로 종료할 수 있음
종료 제한 시간과 Kubernetes 동작
- 종료 시그널을 받은 뒤 애플리케이션이 실제로 쓸 수 있는 종료 시간을 먼저 알아야 함
- Kubernetes의 기본 grace period는
terminationGracePeriodSeconds를 따로 지정하지 않으면 30초임 - 이 시간이 지나면 Kubernetes는
SIGKILL을 보내 애플리케이션을 강제로 중단함SIGKILL은 잡거나 처리할 수 없음
- 남은 요청 처리와 자원 해제까지 포함해 모든 종료 로직은 이 시간 안에 끝나야 함
- 기본 30초를 기준으로 약 20%를 안전 마진으로 남기면, 전체 종료는 25초 안에 끝내는 편이 좋음
새 요청 차단과 readiness 처리
- Go의
net/http에서는http.Server.Shutdown으로 Graceful Shutdown을 수행할 수 있음- 새 연결 수락을 중단함
- 활성 요청이 완료될 때까지 기다림
- 이후 idle connection을 닫음
- 이미 진행 중인 요청은 완료할 수 있고, 완료 뒤 해당 연결은 idle 상태가 되어 닫힘
- 종료 중 새 연결을 시도하는 클라이언트는 리스너가 이미 닫혀 있어 보통
connection refused오류를 받음 - 컨테이너 환경이나 외부 로드밸런서가 있는 오케스트레이션 환경에서는 새 요청 수락을 즉시 중단하지 않는 편이 중요함
- pod가 종료 대상으로 표시된 뒤에도 잠시 트래픽을 받을 수 있음
- Kubernetes 내부 컴포넌트인
kube-proxy는 pod 상태가Terminating으로 바뀐 것을 빠르게 인지함 - 외부 로드밸런서는 Kubernetes와 독립적으로 자체 헬스체크를 사용하므로 상태 전파에 시간이 필요함
- 트래픽 차단 전파를 기다리는 방식은 두 가지임
preStop훅에서 잠시sleep해 외부 로드밸런서가 pod 종료 상태를 인식할 시간을 줌preStop에 걸린 시간은terminationGracePeriodSeconds에 포함됨
- 코드 수준에서 readiness probe를 실패시키고 잠시 대기함
- Kubernetes뿐 아니라 로드밸런서가 준비 상태를 알아야 하는 다른 환경에도 적용 가능함
- readiness probe는 컨테이너가 트래픽을 받을 준비가 되었는지 주기적으로 확인함
- HTTP 요청, TCP 연결, 명령 실행 같은 방식으로 헬스체크를 수행할 수 있음
- probe가 실패하면 Kubernetes는 pod를 service endpoint에서 제거해 트래픽을 받지 않게 함
- 종료 준비 시
isShuttingDown같은atomic.Bool을 사용해/healthz가 HTTP 503을 반환하도록 만들 수 있음 - readiness 상태를 실패로 바꾼 뒤에는 변경 사항 전파를 위해 몇 초 기다려야 함
- 예시 설정은
periodSeconds: 5이며, 본문 예시는 5초 대기를 사용함 - 정확한 대기 시간은 readiness probe 설정에 따라 달라짐
- 예시 설정은
진행 중인 요청 처리
- shutdown budget에 맞춰
context.WithTimeout으로 제한 시간을 만들고server.Shutdown(ctx)에 전달함 server.Shutdown이 반환되는 경우는 두 가지임- 모든 활성 연결이 닫히고 모든 핸들러 처리가 끝남
- 전달한 context가 핸들러 완료 전에 만료되어 서버가 대기를 포기함
- 어느 경우든
Shutdown은 서버가 요청 처리를 완전히 멈춘 뒤 반환함 - 핸들러는 빠르고 context-aware하게 동작해야 함
- 그렇지 않으면 제한 시간 만료 시 작업 중간에 끊길 수 있음
- 부분 쓰기, 데이터 손실, 일관성 없는 상태, 열린 트랜잭션, 손상된 데이터 같은 문제가 생길 수 있음
- 핸들러에 종료 신호를 전달하는 대표 방식은 두 가지임
- 미들웨어로 각 요청 context에 취소 로직을 주입함
http.Server의BaseContext로 모든 연결에 공유되는 전역 context를 제공함
- HTTP 서버에서 커스터마이즈할 수 있는 context는
BaseContext와ConnContext가 있음- Graceful Shutdown에는 서버 전체에 적용되는 취소 가능한 전역 context를 만들 수 있는
BaseContext가 더 적합함
- Graceful Shutdown에는 서버 전체에 적용되는 취소 가능한 전역 context를 만들 수 있는
- Graceful Shutdown은 함수들이 context 취소를 존중할 때 효과가 있음
context.Background(),time.Sleep()처럼 취소를 무시하는 사용을 피해야 함time.Sleep(duration)은select로time.After(duration)와ctx.Done()을 함께 기다리는 방식으로 대체할 수 있음
- 오래된 Go 버전에서는
time.After가 타이머가 실행될 때까지 메모리를 누수할 수 있음- 이 문제는 Go 1.23 이상에서 수정됨
- 버전이 확실하지 않다면
time.NewTimer와Stop, 그리고 필요 시<-t.C확인을 사용할 수 있음 - 관련 이슈: time: stop requiring Timer/Ticker.Stop for prompt GC
Shutdown과 Close의 차이
- 같은 원리는 HTTP 서버뿐 아니라 서드파티 서비스에도 적용됨
database/sql의DB.Close는 데이터베이스 연결을 닫고 새 쿼리 시작을 막으며, 진행 중인 쿼리가 끝날 때까지 기다림- 핵심은 새 요청이나 메시지를 더 받지 않고, 기존 작업이 정의된 grace period 안에 끝날 시간을 주는 것임
server.Close()는 진행 중인 연결을 기다리지 않고 즉시 종료함- 네트워크를 사용 중인 핸들러는 읽기·쓰기 시 오류를 받음
- 클라이언트는
ECONNRESET이나socket hang up같은 연결 오류를 즉시 받을 수 있음 - 네트워크와 상호작용하지 않는 장기 실행 핸들러는 백그라운드에서 계속 실행될 수 있음
server.Shutdown()이 오류를 반환한 뒤server.Close()를 사용할 수는 있지만, 종료 전략에 따라 달라짐- 종료 신호를 context로 전파하는 방식이 더 신뢰성 있고 graceful한 접근임
중요 자원 해제 순서
- 흔한 실수는 종료 시그널을 받자마자 중요 자원을 해제하는 것임
- 이 시점에는 핸들러와 in-flight 요청이 여전히 해당 자원을 사용할 수 있으므로, 자원 정리는 shutdown timeout이 지나거나 모든 요청이 끝난 뒤로 미뤄야 함
- 많은 경우 프로세스 종료만으로도 운영체제가 자원을 회수함
- Go가 할당한 메모리는 프로세스 종료 시 해제됨
- 파일 디스크립터는 운영체제가 닫음
- 프로세스 핸들 같은 OS 수준 자원도 회수됨
- 명시적 정리가 필요한 경우도 있음
- 데이터베이스 연결은 제대로 닫아야 하며, 열린 트랜잭션은 commit 또는 rollback이 필요함
- 메시지 큐와 브로커는 메시지 flush, offset commit, 클라이언트 종료 알림이 필요할 수 있음
- 외부 서비스는 연결 끊김을 즉시 감지하지 못할 수 있으므로, 수동으로 연결을 닫으면 TCP timeout을 기다리는 것보다 빠르게 정리할 수 있음
- 컴포넌트는 초기화의 역순으로 종료하는 것이 좋은 규칙임
- Go의
defer는 마지막에 등록한 함수가 먼저 실행되므로 이 패턴에 잘 맞음
- Go의
- 메모리 캐시 데이터를 디스크에 써야 하는 경우처럼 일부 컴포넌트는 별도 shutdown routine을 설계해야 함
전체 예시의 흐름
- 전체 예시는
signal.NotifyContext로SIGINT와SIGTERM을 받는 root context를 구성함 /healthz엔드포인트는isShuttingDown이 true이면 HTTP 503과Shutting down을 반환하고, 아니면OK를 반환함- 샘플 요청 핸들러는 2초 뒤
Hello, world!를 반환하거나, 요청 context가 취소되면 HTTP request timeout으로 응답함 BaseContext에는ongoingCtx를 연결해 in-flight 요청이SIGTERM직후 바로 취소되지 않게 함- 종료 시그널을 받으면 다음 순서로 진행함
stop()호출로 추가 기본 처리를 허용함isShuttingDown.Store(true)로 readiness 실패 상태를 만듦_readinessDrainDelay인 5초 동안 readiness check 전파를 기다림_shutdownPeriod인 15초 제한 시간으로server.Shutdown을 호출함stopOngoingGracefully()로 진행 중 context를 취소함Shutdown이 실패하면_shutdownHardPeriod인 3초 동안 강제 취소 대기 시간을 둠