Spring Boot - 안전하게 종료시키기
Spring Boot를 안전하게 종료시키는 방법에 대한 소개이다.
※ 읽기 전 참고!! Spring Boot 2.3.0.RELEASE 이후에는
server.shutdown=graceful
속성을 추가하여 안전하게 종료시킬 수 있다. spring-boot-features.html#boot-features-graceful-shutdown
kill 명령어
우선, Spring Boot를 종료시키기 내용을 언급하기 이전에 kill
이라는 리눅스 명령어에 대해 알아보자.
kill
은 의미하는 바와 같이 죽이는(?) 것과 연관이 있다. 이것은 프로세스를 죽이는 명령어으로 프로세스가 시작되면 부여되는 PID(프로세스 ID)를 활용하면 된다.
kill -9 PID
위 명령어는 프로세스를 종료시킬 때 사용하는 명령어로 많은 블로그나 스택오버플로우에서 가장 많이 언급되는 명령어이자 나 또한 주로 사용하던 명령어이다. 하지만 위 명령어는 권장하지 않는 방법이다.
왜 권장하지 않을까?
여기서 숫자 9는 리소스를 정리하는 핸들러를 지정하지 않고 프로세스를 바로 죽이겠다는 의미이다. 만약, 실행 중인 쓰레드가 있더라도 이를 무시하고 중단하는데 혹시라도 굉장히 중요한 작업 중 이라면 최악의 상황이 일어날 수 있기 때문이다.
숫자는 9 이외에도 다른 숫자도 존재하며 다른 의미를 갖고 있다.
그렇다면 안전하게 죽이는 방법은 뭘까...?
tomcat 종료 스크립트를 찾아보자.
$TOMCAT_HOME/bin/shutdown.sh
$TOMCAT_HOME/bin/catalina.sh stop
# https://github.com/apache/tomcat/blob/ffc4b76e42fd39d88c9417d0ba2b3d697c16f5b5/bin/catalina.sh#L543
kill -15 `cat "$CATALINA_PID"` >/dev/null 2>&1
대부분의 애플리케이션은 1(INT), 2(HUP), 15(TERM)를 이용하여 리소스를 정리하는 핸들러 코드를 실행하고 안전하게 종료가 가능하다. 일반적으로 15를 사용한다.
Spring Boot 종료시키기
준비하기
우선 편리한 테스트를 위해 긴 작업 상태를 유지하기 위한 메소드를 작성해보자.
@GetMapping
public String pause() throws InterruptedException {
Thread.sleep(5_000L);
return "Process finished";
}
그리고 긴 작업 중간에 Spring Boot 웹 애플리케이션을 종료시켜 보겠다. 추측으로는 5초를 기다렸다가 'Process finished'라는 문구가 표시될 것이라고 예상된다. 하지만, 예상대로 동작하지 않는다.
curl http://localhost:8080
5000ms 이내에 아래 명령어 실행하자.
kill -15 PID
그 이유는 리소스를 정리할 핸들러는 주어졌지만 Spring Boot에서 리소스를 정리하는 핸들러 코드가 존재하지 않기 때문이다.
Spring - ContextClosedEvent
ContextClosedEvent
를 활용해서 리소스를 정리하는 핸들러 코드를 추가 할 수 있고, 이 방법은 'Marcos Barbero's Blog'를 참고하였다. 간단히 설명하자면, request에 활용되는 ThreadPool를 리소스가 정리될 때까지 기다린 후 내린다. 그 이후에도 내려가지 않는다면 강제로 내려버리는 코드이다.
@Override
public void onApplicationEvent(ContextClosedEvent event) {
this.connector.pause();
Executor executor = this.connector.getProtocolHandler().getExecutor();
if (executor instanceof ThreadPoolExecutor) {
try {
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
threadPoolExecutor.shutdown();
if (!threadPoolExecutor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)) {
log.warn("Tomcat thread pool did not shut down gracefully within "
+ TIMEOUT + " seconds. Proceeding with forceful shutdown");
threadPoolExecutor.shutdownNow();
if (!threadPoolExecutor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)) {
log.error("Tomcat thread pool did not terminate");
}
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
PID 구하기
PID는 어디서 얻을 수 있을까?
여러가지 방법이 있겠지만 Spring Boot에서는 ApplicationPidFileWriter
라는 클래스를 제공해주고 있으며 이를 이용히여 pid가 담긴 파일을 만들어준다. 자세한 내용은 공식 문서를 참고하자.
public static void main(String[] args) {
SpringApplication application = new SpringApplicationBuilder()
.sources(SpringBootGracefulShutdownApplication.class)
.listeners(new ApplicationPidFileWriter("./application.pid"))
.build();
application.run(args);
}
application.pid
는 jar파일과 같은 경로에 만들어진다.