본문 바로가기
💪 Practice

💪우아한 종료Graceful shutdown을 위하여

by iirin 2024. 4. 12.

정상 종료란 무엇일까요?

  • OS로부터 종료 시그널을 받으면 새로운 요청을 받지 않고 처리되고 있던 요청이 모두 끝난 후 모든 자원을 릴리즈한 뒤 프로세스를 종료하는 것입니다.
  • 다른 말로는 그레이스풀 셧다운 Graceful shutdown이라고 하기도 합니다.
Shutdown gracefully shuts down the server without interrupting any active connections. Shutdown works by first closing all open listeners, then closing all idle connections, and then waiting indefinitely for connections to return to idle and then shut down. If the provided context expires before the shutdown is complete, Shutdown returns the context's error, otherwise it returns any error returned from closing the [Server](https://pkg.go.dev/net/http#Server)'s underlying Listener(s).

종료는 활성 연결을 중단하지 않고 서버를 정상적으로 종료합니다. 셧다운은 먼저 열려 있는 모든 수신기를 닫은 다음 유휴 연결을 모두 닫은 다음 연결이 유휴 상태로 돌아갈 때까지 무한정 기다린 다음 종료하는 방식으로 작동합니다. 종료가 완료되기 전에 제공된 컨텍스트가 만료되면 종료는 컨텍스트의 오류를 반환하고, 그렇지 않으면 서버의 기본 리스너를 닫을 때 반환된 모든 오류를 반환합니다.

- [참고링크](https://pkg.go.dev/net/http#Server.Shutdown)

 

  • 왜 정상적으로 종료해야할까요?
    • 애플리케이션의 신뢰도를 높이고 비즈니스 로직을 해치지 않기 위해서 입니다.
    • 저장이 필요한 중요한 데이터, 무조건 트랜젝션 안에 한 번 실행해야 하는 로직(송금 등)의 안정성을 높입니다.

 

 

우아한 종료를 위해 처리해야 하는 OS 시그널

위에서 언급하였듯 우아한 종료는 프로그램 실행 중에 OS 시그널을 받으면, 이를 catch하여 처리로직을 구현하는 형태로 애플리케이션의 우아한 종료를 구현할 수 있습니다.

 

OS마다 지원 시그널이 다를 수 있으며 리눅스 기준 `kill -l` 명령어를 통해 확인할 수 있습니다.

 

  • 간섭 시그널 `SIGINT`
    • 예를 들어 명령줄에서 ctrl + c 를 누른 경우
  • 종료 시그널 `SIGTERM`
    • 예를 들어 외부로부터 어플리케이션 컨테이너 종료 시그널을 받는 경우

 

kill -15 {프로세스의 PID}

 

  • 프로세스 종료 시그널 `SIGKILL`
    • 말 그대로 강제로 프로세스를 종료하는 시그널
    • 시그널을 catch 하는 것이 불가능합니다.
    • 해당 시그널의 핸들러는 만들 수 없습니다.

 

아래는 직접 실행중인 서버에서 시그널을 받았을 때 golang으로 작성된 처리 예시입니다. (참고 : Go로 배우는 웹 애플리케이션 개발 예제)

여담이지만 golang은 종종 모든 것을 직접 구현해줘야할 때가 있어서 동작 원리예제에 대한 예시를 찾기 상대적으로 쉬운 것 같아요.

 

import (
	"context"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"syscall"

	"golang.org/x/sync/errgroup"
)

func (s *Server) Run(ctx context.Context) error {
	ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	eg, ctx := errgroup.WithContext(ctx)
	eg.Go(func() error { // 다른 고루틴에서 HTTP 서버를 실행
		// http.ErrServerClosed 는 정상적인 종료를 의미한다.
		// http.Server.Shutdown() 이 호출되면 http.ErrServerClosed 가 반환되기 때문
		if err := s.srv.Serve(s.l); err != nil &&
			err != http.ErrServerClosed {
			log.Printf("failed to close: %+v", err)
			return err
		}
		return nil
	})

	// 채널로부터의 알림(종료 알림)을 기다린다. 시그널이나 취소 요청에 의해 취소될 때까지 블록된 상태로 있다.
	<-ctx.Done()
	// 모든 요청을 처리한 후 서버를 종료한다.
	if err := s.srv.Shutdown(context.Background()); err != nil {
		log.Printf("failed to shutdown: %+v", err)
	}
	// 정상 종료를 기다린다.
	return eg.Wait()
}

 

 

  • 스프링부트의 경우
    • Springboot 2.3부터 properties `server.shutdown` 설정을 통해 간단하게 설정해줄 수 있습니다 👍
    • 참고링크
server.shutdown=graceful //default: immediate
spring.lifecycle.timeout-per-shutdown-phase=1m //default timeout: 30s

 


Refs.