스프링 서버가 우아하게 죽는법 "Graceful Shutdown"
제가 속해있던 회사에서는 백엔드 어플레이케이션 서버는 Spring Framework를 사용했었고 Github marster 브렌치 머지 -> Github Action -> Aws ecr -> Aws ecs -> Aws ec2 CI/CD환경으로 구성되어 있었습니다.
다만 ec2에 새롭게 업데이트된 버전으로 docker container를 시작 시킬때는 꼭 ecs를 통해 자동으로 실행하는것이 아닌 개발자가 직접 베스찬 서버를 통해 프라이빗 네트워크의 ec2 하나하나 직접 접근하여 무중단 배포 롤링 방식으로 서버를 리스타트하는 정책으로 진행했었습니다.
추후 블루/그린, 카나리까지 염두하고 있었지만, 아직 서비스가 많이 크지 않았기 때문에 크게 문제 되는 배포 방식은 아녔습니다.
개발자로 첫 업무를 시작한 지 3개월 차 때쯤 문득 궁금증이 든 부분이 있었는데 ec2 도커 컨테이너를 stop 시킨다면 당연히 로드밸런스가 헬스체크하여 건강하지 않은 ec2의 서버로 요청을 전달하지 않는 건 이해되었지만 이미 처리 중인 요청은? 어떻게 처리되는 거지? 라는 의문점이 생기더군요.
그래서 직접 테스트해보았습니다.
테스트 용도로 간단하게 Spring으로 Rest Api를 구성했습니다. 해당 Api를 호출하면 시작 로그 20초의 대기 마지막으로 로그를 출력하고 문자열을 리턴합니다.
만약 20초 요청이 처리되던 도중 서버를 종료해 버린다면?
IDE 서버실행 환경에서 강제 종료했을때 soket hang up에러를 출력하였습니다. 해당 에러는 일반적으로 클라이언트와 서버 간의 연결이 예기치 않게 종료되었을때 발생하는 에러중 하나라고합니다.
그럼 제가 서버를 배포할 때 컨테이너를 Stop 해버린다면? 해당 서버에 응답을 기다리고있는 요청들은 저런 오류가 발생하고 고객 사용성이 현저하게 떨어질 수 있겠다는 생각이 들었습니다.
그러면 다음 생각은? 서버가 종료될 때 현재 처리 중인 요청은 처리하고 서버가 종료되면 안 될까? 찾아보니..
Graceful Shutdown개념이 존재했었습니다!
application.yaml
server:
shutdown: graceful
Spring에서 Graceful 종료 설정은 application.yaml을 통해 간단하게 설정이 가능했습니다.
Spring Boot 설정 같지만 사실 서블릿 컨테이너인 일반적으로 많이 사용되어지는 Was Tomcat설정을 통해 Graceful하게 종료되도록 할 수 있었습니다.
위 설정을 한 후 다시 요청이 진행 중인 상태에서 서버를 종료시켰을 때 다음과 같은 로그 Commencing graceful shutdown. Waiting for active requests to complete를 출력하고 모든 요청을 마친 후 마저 로그를 남기고 종료되는 걸 확인할 수 있습니다.
그러면 서버가 Graceful Shutdown일 때 새롭게 요청이 들어오면 어쩌나란 생각이 있었지만. Graceful Shutdown이 진행 중일 때는 새로운 요청에 대해서는 서버가 더이상 받지않기 때문에 진행 중인 요청에 대해서만 처리하고 안전하게 종료되는 걸 담보할 수 있게 됩니다.
spring:
lifecycle:
timeout-per-shutdown-phase: 60s
그러면 또 만약에 진행 중인 요청이 데드락에 걸려 로직 처리가 무한정이 되버리면? 서버는 절대 안전하게 종료되지 않게 됩니다.
그에 대한 최후의 수단으로 Spring 설정을 통해 Graceful Shutdown 타임아웃을 설정할 수 있습니다.
위 설정을 통해 Graceful Shutdown을 하긴 하지만 60초 이상 요청이 치리되지 않을 때 강제로 종료되게 됩니다.
Thread.sleep 시간을 70초로 설정한 후 Graceful Shutdown을 할 경우 타임아웃 설정 60초를 넘어가기 때문에 요청을 마무리하지 않고 강제 종료되는 부분을 확인할 수 있습니다.
Resolved /org.springframework-web.context.request.async-AsyncRequestNotUsabLeExceptiion: ServletOutputStream failed to flush: java.nio.channels.ClosedChannelException 로그를 남기고 현재 진행 중이던 요청을 강제로 종료합니다.
예리하신 분들은 눈치채셨겠지만 Graceful Shutdown 설정은 was 톰켓 설정 이지만 타임아웃 설정은 Spring에 대한 설정인걸 확인하실 수 있는데 이러한 이유는 진행 중인 요청에 대한 권한은 스프링 레벨이기 때문입니다.
IDE 서버 실행 환경에서 확인했으니 도커로 이미지를 만들고 컨테이너를 실행시켜 docker 실행환경으로 Graceful Shutdown 환경을 구성해보겠습니다.
위 도커 Graceful shutdown테스트는 영상으로 대체하겠습니다.
IDE 환경에서와 같이 Graceful Shutdown이 동작하는 부분을 확인하실 수 있습니다.
다만 여기서 주의해야 할 점은 타임아웃 설정을 Spring에서 60초로 설정하였지만 docker stop container 타임아웃이 기본 10초입니다. 따라서 docker stop -t 60 <container_name_or_id> 처럼 타임아웃을 명시하지 않으면 무조건 10초의 타임아웃을 같기에 주의해야 합니다. 타임아웃은 하향평준화라는걸 잊지 말아야 합니다!
docker stop 명령어를 통해 kill -15 SIGTERM 시그널을 프로세스로 전달하고 종료를 진행하게 됩니다. 단 60초 동안 종료를 마무리하지 못하게 된다면 kill -9 SIGKILL 시그널을 통해 프로세스를 강제로 종료하게 됩니다.
잘몬된 부분이 있다면 언제든지 댓글 부탁드리겠습니다!
출처: https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html
Graceful Shutdown :: Spring Boot
Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and servlet-based web applications. It occurs as part of closing the application context and is performed in the earliest
docs.spring.io