코딩은 내일부터

우아하게 서버 종료하기 본문

카테고리 없음

우아하게 서버 종료하기

zl존 비버 2024. 4. 22. 16:54
728x90

서버가 실행되고 있는 상태에서 서버를 종료시키면 어떻게 될까?

이번에는 Spring Boot서버가 어떻게 종료되고 더 나은 방향으로 개선하는 방법을 알아보겠다.

Graceful shutdown

Graceful shutdown란 프로그램을 종료할 때 최대한 side effect가 없도록 로직들을 처리하고 종료하는 걸 말한다.

저번에 블루 그린배포를 하면서 Docker가 자동으로 Graceful shutdown을 해주는 걸로 잘못 알고 있었다.

이번에는 내부구조와 어떻게 돌아가는지 알아보겠다.

시나리오 1 - 일반적인 종료

밑에 사진을 보면 Thread sleep으로 해당 메소드가 30초 소요되도록 작성했다.

이렇게 로직을 실행하고 프로그램을 종료하면 log에 start만 찍히고 end는 안 찍히는 걸 볼 수 있다.

시나리오 2 - Graceful shutdown 적용 (kill - 9)

application 설정파일에 shutdown 방식을 graceful로 바꾸면 된다.

server.shutdown= graceful
    ## 기본 값은 immediate
spring.lifecycle.timeout-per-shutdown-phase= 30s
    ## 기본 값은 30s

이렇게하면~~ 제대로 동작하지 않는 왜 그럴까?

리눅스가 프로세스을 종료하는 방식

Linux에서 프로세스를 종료하는 방법은 밑에와 같이 다양합니다.

$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5 SIGTRAP
6 SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 
63) SIGRTMAX-1 64) SIGRTMAX

여기서 일반적으로 사용하는 명령어는 kill -9, 15이다. 9,15 명령어의 대해 알아보면

  • 9(SIGKILL): 프로세스를 즉시 종료. 처리중이던 작업들을 즉시 종료한다.
  • 15(SIGTERM): 프로세스를 정상적으로 종료시킨다. 소프트웨어 프로세스에게 종료하라는 시그널을 준다고 생각하시면 된다.

위에 말을 정리해보면 특정 시그널은 catch 할 수 있다. catch 한 시그널은 시그널 핸들러를 제작할 수 있다는 뜻이고 이 말은 catch이후에 특정 동작을 하거나 하지 않아도 된다는 뜻이다.

이렇게 catch하고 처리하는 것을 시그널 후킹이라고 한다.

그러면 특정 시그널 = SIGTERM명령어 이고 특정 명령어가 아닌 SIGKILL명령어는 catch 할 수 없어 바로 종료된다는 말로 해석할 수 있다.

자 그러면 kill- 15명령어로 프로세스를 종료시켜 보자.

시나리오 3 - Graceful shutdown 적용 (kill - 15)

15 명령어로 프로세스를 종료를 해보면 Graceful shutdown이 적용되는 것을 볼 수 있다.

참고로 docker compose down 또는 docker down 명령어가 실행되면 SIGTERM시그널이 나간다.

내부동작 알아보기

그러면 Spring Boot는 어떻게 동작을 하고 있는지 알아보겠다.

tomcat패키지에 TomcatWebServer클래스를 보면 설정의 따라 GracefulShutdown객체를 초기화하는 것을 볼 수 있다.

그리고 정상적으로 프로세스를 실행하고 종료할 때 Tomcat이 종료될 때 shutDownGracefully메서드가 실행된다. shutdown설정을 graceful로 해놨기 때문에 GracefulShutdown객체의 shutDownGracefully메서드로 들어가 보겠다.

 

하나하나 살펴보면

  1. doShutdown메서드 내부에서 connector들을 forEach문으로 close를 한다. 이렇게 하면 요청을 더 이상 받지 않는다.
  2. awaitInactiveOrAborted 메서드가 실행되고 while문으로 isActive 즉, 처리 중인 요청이 있는지 확인한다. 만약에 처리중인 요청이 있다면 Thread.sleep을 사용해서 50ms 기다리고 다시 while문이 실행된다.
  3. isActive 함수는 ((StandardWrapper) wrapper). getCountAllocated()를 통해 진행 중인 요청이 몇 개인지 반환하고 0보다 크다면 true를 반환해서 while문이 돌게 된다. → Tomcat은 getCountAllocated메서드를 통해 실행되고 잇는 요청을 확인한다.

그러면 실행되는 요청이 끝나면 프로세스가 종료되는데 만약 종료가 안되면 계속 프로세스가 실행되는가? 결론은 아니다.

맨 처음에 설정파일을 보면 timeout-per-shutdown-phase로 shutdown의 최대 대기 시간을 지정해 줄 수 있다.

 

 

이렇게 설정을 해주면

시간이 지나면 aborted가 true로 바뀌어서 while문이 끝나게 된다.

시나리오 4 - 비동기 스레드 Graceful shutdown

그러면 비동기 스레드를 이용해서 Graceful shutdown을 진행해 보겠다.

결과는 아래와 같이 에러가 뜬다 왜일까?

먼저 비동기의 정의를 보면

  • 비동기: 먼저 시작된 작업의 완료 여부와는 상관없이 새로운 작업을 시작하는 방식

정의와 위에 스택 트레이스랑 같이 보면 쉽게 알 수 있다.

처음 start → end → start - async 순으로 로그가 찍힌다.

즉, GracefulShutdown클래스에 isActive메서드가 실행 중인 요청을 찾지만 hello메서드는 응답이 나가고 async매소드가 비동기 스레드로 메서드를 실행하고 있다. → GracefulShutdown은 이를 알 수 없다는 뜻이다.

이러한 상황에서 해결할 수 있는 방법이 없을까?..

해결방법

정답은 ThreadPoolTaskExecutor의 setWaitForTasksToCompleteOnShutdown와 setAwaitTerminationSeconds 설정을 통해 해결할 수 있다.

위와 같이 설정했다면 이렇게 정상적으로 spring boot가 백그라운드 스레드까지 종료된 걸 확인하고 정상적으로 종료할 것이다.

 

이상!