eelseungmin

[Spring] Graceful Shutdown

by eelseungmin

들어가며

모두의 회식 백엔드에는 운영을 대비해서 무중단 배포를 적용했었다. 즉, 새로운 버전의 코드가 배포되더라도 사용자 입장에서는 마치 끊김 없이 서비스가 지속되는 것처럼 보이기 위한 것이다.

여러 전략 중 Blue/Green 배포 전략을 선택했는데, 백엔드에선 하나의 인스턴스를 사용하고 있으며 인스턴스를 추가하지 않고도 Nginx가 가리키는 컨테이너만 변경하는 간단한 방식이 적합하다고 생각했기 때문이다.

 

무중단 배포에 대한 조사를 하면서 고민이 필요한 부분이 있었는데 아래에서 이어가도록 하겠다.

 

Graceful Shutdown

배포가 이루어질 때 구버전의 서버에서 요청을 처리하다가 응답이 완료되지 않은 시점에 구버전으로 향하는 포트가 끊기고 신버전의 서버가 실행된다고 가정하자.

이때 처리가 끝나지 않은 요청은 어떻게 될까?

 

한 번 실험해 보자.

 

Spring Web 프로젝트를 1개 생성하고 간단한 컨트롤러를 다음과 같이 구성했다. 

@Slf4j
@RestController
public class ShutdownController {

    @GetMapping("/api/test")
    public String test() throws InterruptedException {
        log.info("Start");

        Thread.sleep(5000);

        return "Finish";
    }
}

시작 로그를 하나 남기고 비즈니스 로직 과정을 해당 스레드를 sleep 시키는 코드로 대체한 뒤 처리가 완료되면 "Finish"가 화면에 전송되도록 했다.

 

옵션 적용 전

처음은 서버에 별다른 옵션을 적용하지 않은 상태로 처리 도중에 서버가 종료되도록 해보자.

예상하던 대로 원하는 응답이 오지 않은 채 서버가 종료되어 버린다.

 

옵션 적용 후

그렇다면 이번엔 application.properties에 다음 옵션을 적용한 뒤 똑같이 해보도록 하겠다.

server.shutdown=graceful

이번엔 요청이 처리된 후 서버가 종료된다.

 

Spring Web 공식문서에서 해당 옵션에 대한 내용을 확인할 수 있다.

This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted.

간단히 말해 이 옵션을 주게 되면 종료 이전에 기존 요청이 완료되도록 유예를 줄 수 있다고 한다.

 

스프링에선 이러한 동작을 어떻게 구현한 걸까? 다음은 내 나름대로의 추측을 정리해 보았다.

 

Graceful Shutdown 구현 방식에 대한 추측

설명하기에 앞서 리눅스에는 시그널이라는 개념이 있는데, 이는 프로세스에 특정 이벤트 발생을 알리기 위한 수단이다.

프로세스는 시그널을 수신하면 다음과 같은 3가지 행동 중 하나를 취할 수 있음을 숙지하고 넘어가자.

(다만 어떤 행동을 취할 수 있는지는 시그널 종류에 따라 제각각이다.)

  1. ignore it : 무시하기
  2. catch it : 시그널에 적합한 함수 호출하기
  3. accept the default : default 값을 수행하도록 내버려두기

 

서버가 종료될 때 기록되는 다음 로그를 통해 일련의 과정에 대해 힌트를 얻을 수 있다.

Process finished with exit code 130

조사에 따르면, bash에서 사용하는 130번 코드를 통해 애플리케이션이 종료되었음을 의미하는데 이는 리눅스 관점에서 보면 SIGTERM에 의한 종료라고 한다.

 

이 글을 읽는 분들이라면 리눅스 환경에서 프로세스를 종료하기 위해 "kill -9 PID" 같은 명령어를 사용해 본 경험이 있을 것이다. kill 명령어 뒤에 위치한 -9 옵션은 SIGKILL 옵션으로서, 프로세스를 말 그대로 곧바로 종료해 버릴 뿐만 아니라 이에 대한 추가적인 조작이나 무시가 불가능하다.

 

반면 SIGTERM은 프로세스한테 정상적인 종료를 요청하는 명령어로서 SIGKILL과 다른 점은 종료하기 전에 어떠한 로직이 처리되도록 할 수 있다는 점이다. 즉 위에서 언급했던 2번의 선택지가 추가되는 것이다.

 

리눅스에선 2번에 대한 동작을 정의할 수 있도록 시그널 핸들러라는 도구를 제공한다고 한다.

그렇다면 스프링부트 내장 웹서버인 톰캣에서는 graceful 옵션이 인식될 경우 종료되기 전에 요청이 완료되도록 시그널 핸들러 조작을 통해 Graceful Shutdown을 구현했다고 생각해 볼 수 있겠다.

 

참조

https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html#web.graceful-shutdown

https://linuxhandbook.com/sigterm-vs-sigkill/

https://stackoverflow.com/questions/29887088/java-program-exit-with-code-130

블로그의 정보

eel.log

eelseungmin

활동하기