preview image
허원철의 개발 블로그

Spring Boot - 안전하게 종료시키기

2018-12-28

Spring Boot를 안전하게 종료시키는 방법에 대한 소개이다.

kill 명령어

우선, Spring Boot를 종료시키기 내용을 언급하기 이전에 kill이라는 리눅스 명령어에 대해 알아보자.

kill은 의미하는 바와 같이 죽이는(?) 것과 연관이 있다. 이것은 프로세스를 죽이는 명령어으로 프로세스가 시작되면 부여되는 PID(프로세스 ID)를 활용하면 된다.

1
kill -9 PID

위 명령어는 프로세스를 종료시킬 때 사용하는 명령어로 많은 블로그나 스택오버플로우에서 가장 많이 언급되는 명령어이자 나 또한 주로 사용하던 명령어이다. 하지만 위 명령어는 권장하지 않는 방법이다.

왜 권장하지 않을까?

여기서 숫자 9는 리소스를 정리하는 핸들러를 지정하지 않고 프로세스를 바로 죽이겠다는 의미이다. 만약, 실행 중인 쓰레드가 있더라도 이를 무시하고 중단하는데 혹시라도 굉장히 중요한 작업 중 이라면 최악의 상황이 일어날 수 있기 때문이다.

숫자는 9 이외에도 다른 숫자도 존재하며 다른 의미를 갖고 있다.


그렇다면 안전하게 죽이는 방법은 뭘까…?

tomcat 종료 스크립트를 찾아보자.

1
2
$TOMCAT_HOME/bin/shutdown.sh
$TOMCAT_HOME/bin/catalina.sh stop
1
2
# 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 종료시키기

준비하기

우선 편리한 테스트를 위해 긴 작업 상태를 유지하기 위한 메소드를 작성해보자.

1
2
3
4
5
@GetMapping
public String pause() throws InterruptedException {
Thread.sleep(5_000L);
return "Process finished";
}

그리고 긴 작업 중간에 Spring Boot 웹 애플리케이션을 종료시켜 보겠다. 추측으로는 5초를 기다렸다가 ‘Process finished’라는 문구가 표시될 것이라고 예상된다. 하지만, 예상대로 동작하지 않는다.

1
curl http://localhost:8080

5000ms 이내에 아래 명령어 실행하자.

1
kill -15 PID

Alt error 발생

그 이유는 리소스를 정리할 핸들러는 주어졌지만 Spring Boot에서 리소스를 정리하는 핸들러 코드가 존재하지 않기 때문이다.

Spring - ContextClosedEvent

ContextClosedEvent를 활용해서 리소스를 정리하는 핸들러 코드를 추가 할 수 있고, 이 방법은 ‘Marcos Barbero’s Blog‘를 참고하였다. 간단히 설명하자면, request에 활용되는 ThreadPool를 리소스가 정리될 때까지 기다린 후 내린다. 그 이후에도 내려가지 않는다면 강제로 내려버리는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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가 담긴 파일을 만들어준다. 자세한 내용은 공식 문서를 참고하자.

1
2
3
4
5
6
7
8
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파일과 같은 경로에 만들어진다.

참고