JVM Shutdown HookとSpring Boot Graceful Shutdownについてまとめました




- 過去に執筆した記事を日本語に翻訳しました
- 翻訳機を使用しているため、誤字や不自然な表現があるかもしれません

# Graceful Shutdownとは
- Graceful Shutdown（優雅な終了）とは、プログラムやシステムが終了する前に、現在処理中の作業を完了し、リソースを整理し、データの損失や不正確な状態を最小限に抑える終了手順を指します
- システムとデータの安定性を保証し、ユーザーエクスペリエンスを向上させることができます

:::note warn
### Hard Shutdown
- Hard Shutdownとは、強制終了や異常終了を指し、例えば kill -9(SIGKILL) やサーバーの電源供給自体が切れた場合などが該当いたします
- この場合、JVMやアプリケーションの`正常終了ルーチ`が呼び出されず、リソースの解放やロギングなどの後続作業も進行できません
:::


# JVMにおけるJVM Shutdown Hook
- Javaでは、`Runtime.addShutdownHook(Thread hook)`メソッドを活用して、「Graceful Shutdown」を簡単にサポートいたします
- Shutdown Hookとは、JVMが終了する直前（正確にはRuntimeが終了を検知した直後）に、登録された特定のロジックを実行する機会を提供する機能でございます

ここには2つの状況が存在いたします。


> The Java virtual machine shuts down in response to two kinds of events:
The program exits normally, when the last non-daemon thread exits or when the exit (equivalently, System.exit) method is invoked, or
The virtual machine is terminated in response to a user interrupt, such as typing ^C, or a system-wide event, such as user logoff or system shutdown.


1. 最後の non-daemon スレッドが終了するか、System.exit() メソッドが呼び出されてプログラムが正常に終了する場合
2. ユーザーのインタラプト（^C入力）やユーザーログオフ、システム終了などのシステム全体のイベントによりJVMが終了する場合

## 正常終了時の Shutdown hook 

### メインスレッド（最後の non-daemon スレッド）が終了する場合
- すべての non-daemon スレッドが終了した後、JVMが終了する際に Shutdown hook が動作いたします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/77b2b650-2748-4931-b9e3-1abf49a9524b.png)
 -> 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/0fd7b90e-716b-445b-be99-a86245a15203.png)

### System.exit(0) メソッドが呼び出された場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/21de3b3d-01d9-48c8-ae82-0cbb114e5a0a.png)
-> 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/564dca63-fd42-41c1-b09c-b439e5f28c17.png)

## システム全体のイベントによる終了時の Shutdown Hook

### SIGINTシグナル 
- ユーザーインタラプト（Ctrl+C）の送信や、直接コマンドで SIGTERM を送信した場合に Shutdown Hook が実行されます
- プロセスがこれを正常に処理すると、Shutdown Hook が実行される可能性がございます

:::note warn
### SIGKILLシグナル
- SIGKILLシグナル（`kill -9`）はプロセスを即時に終了させるため、Shutdown Hook は実行されません
- プロセス終了により、いかなるシグナル処理（ハンドラー）も行うことができません
:::

:::note info
### SIGTERM
- プロセスに終了要求を送信し、プロセスはこのシグナルを捕捉して、終了前に必要な整理作業（リソースの解放、ファイルの保存など）を実行する機会を持ち、Shutdown Hook も実行されます
- コード内でこのシグナルを処理または無視することが可能です
- Linuxでは、特別な番号指定なしに "kill" コマンドを使用すると、基本的に SIGTERM が送信されます
- `kill <pid>` を入力するか、ユーザーインタラプト（Ctrl+C）を送信するか、またはシステム終了時に `SIGTERM` シグナルが送信されます
:::

https://en.wikipedia.org/wiki/Signal_(IPC)

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Runtime.html#addShutdownHook(java.lang.Thread)

# Spring BootにおけるGraceful Shutdown
Spring Bootでは、2.3バージョン以降、「Graceful Shutdown」という機能を導入いたしました。

- 基本的に、4種類の組み込みWebサーバー（Jetty、Reactor Netty、Tomcat、Undertow）および、リアクティブとサーブレットベースのWebアプリケーションで有効となります

```yaml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
```

### server.shutdown=graceful
- このオプションを利用することで、graceful shutdownモードを有効化することができます

### `spring.lifecycle.timeout-per-shutdown-phase`
- このプロパティを用いることで、shutdown phaseが完了するまでの最大待機時間を指定することが可能です

## テスト
- `timeout-per-shutdown-phase: 30s` として30秒に設定した後、テストを行ってみましょう

```java
@RestController
public class Controller {

    @GetMapping("/test")
    public ResponseEntity shutDownTest() throws InterruptedException {

        Thread.sleep(20000);
        System.out.println("wowowowowowoowowowow!!!");
        return ResponseEntity.ok().build();
    }
}
```
- 次のように定義し、該当APIにリクエストを送信した直後にサービスを終了した場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/b6053aea-6d85-4e8e-b4d3-e299b020fc7a.png)


- 20秒かかる作業がすべて完了し、returnされた後に終了することが確認できます

```java
 @GetMapping("/test")
    public ResponseEntity shutDownTest() throws InterruptedException {

        Thread.sleep(1000000);
        System.out.println("wowowowowowoowowowow!!!");
        return ResponseEntity.ok().build();
    }
```
次のように長時間かかるロジックがある場合、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/5cf323d7-b0bc-43c5-83c6-7fd76a6d3497.png)

```markdown
Shutdown phase 2147482623 ends with 1 bean still running after timeout of 30000ms: [webServerGracefulShutdown]
```
30秒待ったにもかかわらず終了せず、

```markdown
Graceful shutdown aborted with one or more requests still active
```
待機時間中にリクエストがすべて完了しなかったため、Graceful Shutdownを完全に実行できず、強制終了（Abort）処理が行われたことが確認できます。

## Graceful Shutdownの内部実装

```java
/*
 * Copyright 2012-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.web.server;

/**
 * A callback for the result of a graceful shutdown request.
 *
 * @author Andy Wilkinson
 * @since 2.3.0
 * @see WebServer#shutDownGracefully(GracefulShutdownCallback)
 */
@FunctionalInterface
public interface GracefulShutdownCallback {

	/**
	 * Graceful shutdown has completed with the given {@code result}.
	 * @param result the result of the shutdown
	 */
	void shutdownComplete(GracefulShutdownResult result);

}
```
- Spring Bootは以下のようなインターフェースを通じてGraceful Shutdownをサポートしております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/0aae6f26-4fee-42d5-83f1-ca52bf78e878.png)

- このインターフェースを継承するさまざまな実装クラスが存在することが確認できます

```java
package org.springframework.boot.web.server;

public interface WebServer {

	/*
	 * Initiates a graceful shutdown of the web server. Handling of new requests is
	 * prevented and the given {@code callback} is invoked at the end of the attempt. The
	 * attempt can be explicitly ended by invoking {@link #stop}. The default
	 * implementation invokes the callback immediately with
	 * {@link GracefulShutdownResult#IMMEDIATE}, i.e. no attempt is made at a graceful
	 * shutdown.
	 * @param callback the callback to invoke when the graceful shutdown completes
	 * @since 2.3.0
	 */
	default void shutDownGracefully(GracefulShutdownCallback callback) {
		callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
	}

}
```
- 別途実装がない場合、即座に終了応答を返すように設定されております
- 各サーバー環境に合わせて、このメソッドを再定義することで、実際にGraceful Shutdownが可能となるよう実装されております

## nettyにおけるGraceful shutdown

```java

private final GracefulShutdown gracefulShutdown;

// ...

public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout,
			Shutdown shutdown, ReactorResourceFactory resourceFactory) {
		Assert.notNull(httpServer, "HttpServer must not be null");
		Assert.notNull(handlerAdapter, "HandlerAdapter must not be null");
		this.lifecycleTimeout = lifecycleTimeout;
		this.handler = handlerAdapter;
		this.httpServer = httpServer.channelGroup(new DefaultChannelGroup(new DefaultEventExecutor()));
		this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(() -> this.disposableServer)
				: null;
		this.resourceFactory = resourceFactory;
	}
// .... 
@Override
	public void shutDownGracefully(GracefulShutdownCallback callback) {
		if (this.gracefulShutdown == null) {
			callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
			return;
		}
		this.gracefulShutdown.shutDownGracefully(callback);
	}
```
- NettyWebServerはGracefulShutdownオブジェクトを通じて実際の終了ロジックを実行し、完了後はGracefulShutdownCallbackを介して結果を通知いたします
- 十分な時間がある場合はすべてのリクエストを処理した後に終了しますが、指定されたタイムアウトを超えると、最終的に強制終了（Abort）処理が実行されます

```java
final class GracefulShutdown {

	private static final Log logger = LogFactory.getLog(GracefulShutdown.class);

	private final Supplier<DisposableServer> disposableServer;

	private volatile Thread shutdownThread;

	private volatile boolean shuttingDown;

	GracefulShutdown(Supplier<DisposableServer> disposableServer) {
		this.disposableServer = disposableServer;
	}

	void shutDownGracefully(GracefulShutdownCallback callback) {
		DisposableServer server = this.disposableServer.get();
		if (server == null) {
			return;
		}
		logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
		this.shutdownThread = new Thread(() -> doShutdown(callback, server), "netty-shutdown");
		this.shutdownThread.start();
	}

	private void doShutdown(GracefulShutdownCallback callback, DisposableServer server) {
		this.shuttingDown = true;
		try {
			server.disposeNow(Duration.ofNanos(Long.MAX_VALUE));
			logger.info("Graceful shutdown complete");
			callback.shutdownComplete(GracefulShutdownResult.IDLE);
		}
		catch (Exception ex) {
			logger.info("Graceful shutdown aborted with one or more requests still active");
			callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
		}
		finally {
			this.shutdownThread = null;
			this.shuttingDown = false;
		}
	}

	void abort() {
		Thread shutdownThread = this.shutdownThread;
		if (shutdownThread != null) {
			while (!this.shuttingDown) {
				sleep(50);
			}
			shutdownThread.interrupt();
		}
	}
```

- `shutDownGracefully(GracefulShutdownCallback callback)`を見ると、別途スレッド `netty-shutdown` を生成してGraceful Shutdownを処理しております

- `server.disposeNow(Duration.ofNanos(Long.MAX_VALUE))` では、Nettyの内部リソースやソケットを閉じると同時に、一定時間リクエスト処理の完了を待ちます

- Spring BootのLifecycle（例：`spring.lifecycle.timeout-per-shutdown-phase`）では、「終了待機時間を超過した場合にAbortを呼び出す」という形で実装されているとご理解いただければと存じます


```java

	/**
	 * Releases or closes the underlying {@link Channel}.
	 */
	@Override
	@SuppressWarnings({"FutureReturnValueIgnored", "FunctionalInterfaceMethodChanged"})
	default void dispose() {
		//"FutureReturnValueIgnored" this is deliberate
		channel().close();
	}

	/**
	 * Releases or closes the underlying {@link Channel} in a blocking fashion with
	 * {@code 3} seconds default timeout.
	 */
	default void disposeNow() {
		disposeNow(Duration.ofSeconds(3));
	}

	/**
	 * Releases or closes the underlying {@link Channel} in a blocking fashion with
	 * the provided timeout.
	 *
	 * @param timeout max dispose timeout (resolution: ns)
	 */
	default void disposeNow(Duration timeout) {
		if (isDisposed()) {
			return;
		}

		requireNonNull(timeout, "timeout");

		dispose();
		try {
			onDispose().block(timeout);
		}
		catch (IllegalStateException e) {
			if (e.getMessage()
			     .contains("blocking read")) {
				throw new IllegalStateException("Socket couldn't be stopped within " + timeout.toMillis() + "ms");
			}
			throw e;
		}
	}
```
- block(timeout) を呼び出してチャネルを閉じる作業をブロックし、この時点以降、新しいリクエストを受け付けないように制御いたします
- 既に処理中のリクエストがすべて完了するまで待機いたしますが、Spring Boot の Shutdown Phase タイムアウトを超過した場合には abort() により強制終了され、未処理のリクエストがあってもこれ以上待機しません

## 参考資料

https://en.wikipedia.org/wiki/Signal_(IPC)

https://docs.oracle.com/javase/8/docs/technotes/guides/lang/hook-design.html

https://javacan.tistory.com/entry/86

https://www.baeldung.com/jvm-shutdown-hooks

https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html