이전 글에서 이어집니다.
배경
이전까지 진행했던 블루-그린 배포를 구현하는 과정은 큰 그림으로써의 무중단 배포를 의미하였습니다. 단순히 배포과정에서 발생하는 다운타임(downtime)만 고려하여 이를 방지하기 위한 수단이었습니다.
이제 조금 더 섬세하게 고려할 부분은 ‘ 오래된 버전의 애플리케이션을 종료할 때 사용자의 요청이 처리중이었다면?’ 입니다.
물론 복구 프로세스를 확립한다든가, 애플리케이션 윗단에서 종료 프로세스를 관리하는 방법(AWS Elastic Beanstalk 등)이 존재하긴 하지만 애플리케이션 자체에서도 정상적인 종료 프로세스는 꼭 필요하다고 생각합니다.
이를 위해 Graceful Shutdown
을 스낵게임에 적용하려고 합니다.
Graceful Shutdown이란?
기본 개념은 아래와 같습니다.
Graceful Shutdown
이 진행되면 더이상 요청은 거부한다.- 처리중인 요청이 있다면 마무리하고 애플리케이션을 종료한다.
Process 종료 시그널
대표적으로는 아래의 세가지 시그널이 존재합니다. 이외에도 다른 시그널들이 존재하지만 여기서는 자주 쓰이는 것만 살펴보겠습니다.
SIGTERM
- 프로세스를 종료 시키기전에 해당 시그널을 핸들링 할 수 있다.
- Graceful Shutdown을 사용하기 위한 시그널이다.
kill -15
SIGINT
- SIGTERM과 동일하지만 트리거가 키보드라는 점에서 차이가 있다.
SIGKILL
- 프로세스가 종료되기 전에 수행되어야 하는 종료절차를 실행하지 않으며 즉시 종료 시킨다.
kill -9
구현
//application.yml
server:
shutdown:graceful //default 값은 immediate
위와 같이 설정파일에서 shutdown 방식을 graceful
로만 설정해주면 됩니다.
테스트를 해보자
- Graceful Shutdown을 적용하지 않고 SIGKILL을 보냈을때
- Graceful Shutdown을 적용하고 SIGTERM을 보냈을때
이런 경우에는 Graceful Shutdown이 정상적으로 작동하지 못하여 영원히 프로세스가 종료되지 않을 것입니다. 따라서 이런 문제를 방지하기 위해 타임아웃을 설정해두면 됩니다.
//application.yml
spring:
lifecycle:
timeout-per-shutdown-phase: 10s
//팀원분이 10s보다 더 긴 작업이 있으면 중간에 끊길 수 있다고 어떤 기준에 의해 정해졌는지 물어보셨습니다.
//처음 생각한것은 애초에 응답에 10s 이상 걸리는 API가 존재하면 안된다는 것이었는데 이것은 추측일 뿐
//정확한 진단이 아니었습니다.
//당장은 정확한 진단이 어려워보여 일단 설정해두고 후에 조치를 취하려합니다.
고민점
그렇다면 Graceful Shutdown중에 들어오는 요청에 대한 처리는?
요청 1 -> Graceful Shutdown -> 요청 2
위에서 설명한 바와 같이 이런 상황에서 요청 2는 네트워크 레벨에서 차단을 당합니다. 처음에는 요청 2를 차단하면 원래 목적인 무중단 배포에 부합하지 않는게 아닐까라는 바보같은 생각을 하였습니다.
하지만 다시 천천히 생각을 해보니 저희는 오래된 버전의 애플리케이션을 종료하고 새로운 버전의 애플리케이션을 시작하지 않습니다.
대신 새로운 버전의 애플리케이션 시작 후 리버스 프록시 설정을 변경하여 모든 요청이 새로운 버전의 애플리케이션으로 향하게 해두었고 이 과정들이 끝난 후에 오래된 버전의 애플리케이션을 종료하기에 걱정했던 부분이 문제가 되지 않는다고 판단하였습니다.
3. 어라? 그러면 Nginx의 reload중 다운타임은..?
처음에는 Nginx가 알아서 old worker process들에게 graceful shutdown을 요청한다기에 zero-downtime이라고만 생각했습니다..
하지만 조금 더 찾아보니 keep-alive 옵션이 켜져있는 클라이언트에게는 해당되지 않는것 같습니다…
이 이상은 글이 너무 길어질 것 같아 nginx와 관련된 내용은 추후에 더 공부해보도록 하겠습니다.
동작 원리
아래의 동작원리는 Tomcat을 기준으로 작성되었습니다.
먼저 SpringBoot가 시작되면 위와 같이 GracefulShutdown
객체를 생성하여 할당합니다.
이후 종료시에는 shutDownGracefully()
가 호출되는데 다음과 같습니다.
doShutdown()
메서드는 먼저, 현재 열려있는Connector
들을 찾아 닫고 새로운 요청을 차단합니다.
그 후, 50ms마다 isActive()
로 현재 처리중인 요청을 확인을 합니다.
결론
지금까지 길고 긴 무중단 배포 적용기가 끝났습니다. 처음에는 무중단 배포라는 것이 그저 방법론에 맞춰 인프라만 구성해주면 될 것 같다고 생각하였습니다. 하지만 막상 직접 해보니 그 속에서 세세하게 신경써야 할 부분들이 존재하였습니다.
이번 작업은 어플리케이션의 시작과 진행 중 에만 몰두돼 있던 저희에게 종료 에도 관심을 기울일 수 있는 귀중한 기회였던 것 같습니다.