これは、なにをしたくて書いたもの?
Spring Boot 2.3から追加されている、Graceful Shutdownが前々から気になっていたので、ちょっと試してみようと
思います。
Spring Boot 2.3.0 available now
Graceful Shutdown
Spring BootのGraceful Shutdownは、こちらに記載されています。
Spring Boot Features / Graceful shutdown
Graceful Shutdownは、Tomcat、Jetty、Undertow、Reactor Nettyの4つの組み込みサーバー、Servlet、Reactiveの両方で
サポートされている機能です。
※Tomcatに関しては、9.0.33以降が必要です
アプリケーションを停止する際に、すでに実行されているリクエストの完了は待つものの、新しいリクエストは
受け付けなくなります。また、停止するまでの猶予期間を指定できます。
新しいリクエストを受け付けなくなる方法は、サーバーごとに異なります。
Graceful Shutdownは、デフォルトでは有効になっていません。
まず、server.shutdown
プロパティをgraceful
に設定する必要があります。
Common Application Properties / Server Properties / server.shutdown
デフォルトは、すぐに停止するimmediate
です。
Graceful Shutdownで停止するまでの猶予期間は、spring.lifecycle.timeout-per-shutdown-phase
プロパティで指定します。
Common Application Properties / Core Properties / spring.lifecycle.timeout-per-shutdown-phase
デフォルト値は30秒です。
そして、ドキュメントにも書かれていますが、Graceful ShutdownはSmartLifecycle
を実装したBeanによる停止処理の
早い段階で行われるようです。
SmartLifecycle (Spring Framework 5.3.10 API)
説明はこれくらいにして、実際に試してみましょう。
環境
今回の環境は、こちらです。
$ java --version openjdk 11.0.11 2021-04-20 OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04) OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-88-generic", arch: "amd64", family: "unix"
プロジェクト作成
Spring Initializrで、プロジェクトを作成します。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=2.5.5 \ -d javaVersion=11 \ -d name=container-graceful-shutdown \ -d groupId=org.littlewings \ -d artifactId=container-graceful-shutdown \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring \ -d dependencies=web \ -d baseDir=container-graceful-shutdown | tar zxvf -
作成したプロジェクト内に移動。
$ cd container-graceful-shutdown
依存関係は、これだけです。今回はspring-boot-starter-web
でデフォルトで組み込まれているTomcatで試すことにします。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
生成されているソースコードは、いったん削除。
$ rm src/main/java/org/littlewings/spring/ContainerGracefulShutdownApplication.java src/test/java/org/littlewings/spring/ContainerGracefulShutdownApplicationTests.java
起動クラス+RestControllerを作成する
起動クラスは、こちら。
src/main/java/org/littlewings/spring/App.java
package org.littlewings.spring; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class App { Logger logger = LoggerFactory.getLogger(App.class); public static void main(String... args) { SpringApplication.run(App.class, args); } @GetMapping("hello") public String hello(@RequestParam(defaultValue = "10") int sleepTime) throws InterruptedException { logger.info("accept, sleep time = {} sec", sleepTime); TimeUnit.SECONDS.sleep(sleepTime); return "Hello World!!"; } }
RestControllerは、/hello
でHello World!!
を返しつつ、間にスリープする実装にしています。
@GetMapping("hello") public String hello(@RequestParam(defaultValue = "10") int sleepTime) throws InterruptedException { logger.info("accept, sleep time = {} sec", sleepTime); TimeUnit.SECONDS.sleep(sleepTime); return "Hello World!!"; }
スリープする時間は、デフォルトでは10秒としています。
Immediate Shutdown(デフォルト)で確認する
最初は、Immediate Shutdown(デフォルト)で確認してみましょう。
まずはパッケージング。
$ mvn package
起動。
$ java -jar target/container-graceful-shutdown-0.0.1-SNAPSHOT.jar
確認。
$ time curl localhost:8080/hello Hello World!! real 0m10.271s user 0m0.006s sys 0m0.011s
レスポンスが返ってくるまでに、10秒かかります。
再度、リクエストを投げてみます。
$ time curl localhost:8080/hello
ここでレスポンスが返ってくるまでに、kill
してみます。
$ kill `ps -ef | grep 'java -jar' | grep -v grep | awk '{print $2}'`
curl
にはレスポンスが返ってきませんでした。
$ time curl localhost:8080/hello curl: (52) Empty reply from server real 0m4.709s user 0m0.008s sys 0m0.000s
そして、アプリケーション側はkill
した時点で停止します。
これが通常の動きですね。
Graceful Shutdownを有効にする
では、Graceful Shutdownを有効にしてみます。application.properties
でserver.shutdown
をgraceful
と設定。
src/main/resources/application.properties
server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=15s
spring.lifecycle.timeout-per-shutdown-phase
は15秒とします。
パッケージングして
$ mvn package
実行。
$ java -jar target/container-graceful-shutdown-0.0.1-SNAPSHOT.jar
とりあえず、確認してみます。
$ time curl localhost:8080/hello Hello World!! real 0m10.129s user 0m0.010s sys 0m0.000s
次に、リクエストを投げて
$ time curl localhost:8080/hello
裏でアプリケーションをkill
。
$ kill `ps -ef | grep 'java -jar' | grep -v grep | awk '{print $2}'`
すると、アプリケーション側ではGraceful Shutdownが始まります。
2021-10-08 23:46:57.524 INFO 22386 --- [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
アプリケーションは停止せず、レスポンスはクライアントに返ってきます。
$ time curl localhost:8080/hello Hello World!! real 0m10.013s user 0m0.010s sys 0m0.000s
そして、アプリケーションが停止します。
2021-10-08 23:47:05.753 INFO 22386 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete
Graceful Shutdownにより、現在処理しているリクエストを待ってからアプリケーションが終了することが確認できました。
次は、Graceful Shutdownの猶予期間中に処理が終わらないリクエストも含めてみます。
起動。
$ java -jar target/container-graceful-shutdown-0.0.1-SNAPSHOT.jar
リクエストを2つ投げておきます。
# 1 request $ time curl localhost:8080/hello # 2 request $ time curl localhost:8080/hello?sleepTime=30
片方は、猶予時間である15秒よりも長くスリープさせます(30秒)。
そしてkill
。
$ kill `ps -ef | grep 'java -jar' | grep -v grep | awk '{print $2}'`
Graceful Shutdownが始まります。
2021-10-08 23:56:56.188 INFO 22669 --- [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
10秒スリープの方は、レスポンスが返ってきます。
# 1 response $ time curl localhost:8080/hello Hello World!! real 0m10.153s user 0m0.010s sys 0m0.000s
30秒スリープさせる方は待ちきれず、アプリケーション側がシャットダウンします。
2021-10-08 23:57:11.189 INFO 22669 --- [ionShutdownHook] o.s.c.support.DefaultLifecycleProcessor : Failed to shut down 1 bean with phase value 2147483647 within timeout of 15000ms: [webServerGracefulShutdown] 2021-10-08 23:57:11.231 INFO 22669 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown aborted with one or more requests still active
この時、処理中のBeanがあるとログに記録されていますね。
クライアント側にも、レスポンスは返りません。
# 2 response $ time curl localhost:8080/hello?sleepTime=30 curl: (52) Empty reply from server real 0m18.422s user 0m0.006s sys 0m0.006s
最後に、アプリケーションのシャットダウンが始まった後にリクエストを投げるパターンも実行してみます。
まずは起動。
$ java -jar target/container-graceful-shutdown-0.0.1-SNAPSHOT.jar
先ほどと同じように、リクエストを2つ(片方は猶予時間中に終わらないもの)を投げておきます。
# 1 request $ time curl localhost:8080/hello # 2 request $ time curl localhost:8080/hello?sleepTime=30
kill
。
$ kill `ps -ef | grep 'java -jar' | grep -v grep | awk '{print $2}'`
Graceful Shutdownが始まります。
2021-10-09 00:05:34.746 INFO 23531 --- [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
この状態で、次のリクエストを投げようとすると、拒否されることが確認できます。
# 3 request $ time curl localhost:8080/hello curl: (7) Failed to connect to localhost port 8080: 接続を拒否されました real 0m0.009s user 0m0.009s sys 0m0.000s
ひとつ目のリクエストは、レスポンスが得られます。
# 1 response $ time curl localhost:8080/hello Hello World!! real 0m10.135s user 0m0.005s sys 0m0.005s
そして、アプリケーションは2つ目のリクエストの終了を待ちきれず、停止。
2021-10-09 00:05:49.747 INFO 23531 --- [ionShutdownHook] o.s.c.support.DefaultLifecycleProcessor : Failed to shut down 1 bean with phase value 2147483647 within timeout of 15000ms: [webServerGracefulShutdown] 2021-10-09 00:05:49.789 INFO 23531 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown aborted with one or more requests still active
2つ目のリクエストは、レスポンスが得られませんでした。
$ time curl localhost:8080/hello?sleepTime=30 curl: (52) Empty reply from server real 0m18.848s user 0m0.006s sys 0m0.006s
これで、Graceful Shutdownに関するおよその振る舞いは確認できたかなと思います。
少し実装を見る
Graceful Shutdownは、SmartLifecycle
インターフェースの実装である、WebServerGracefulShutdownLifecycle
クラスが
アプリケーションの停止時にGraceful Shutdown用の処理を呼び出すことで行われます。
ここで出てくるWebServer
というのが、各組み込みサーバー向けの実装を抽象するインターフェースとなります。
各組み込みサーバーの実装と、Graceful Shutdownの処理を実装しているクラスは、以下のようになっています。
- Tomcat
- Undertow
- UndertowWebServer.java
- Reactive向け
- UndertowServletWebServer.java
- Servlet向け
- UndertowWebServer.java
- Jetty
- Reactor Netty
いずれもGraceful Shutdown用のクラスが切り出されて存在するのですが、UndertowはReactive向けに実装されている
UndertowWebServer
内にGraceful Shutdownの処理が組み込まれています。また、UndertowServletWebServer
は
サーブレット(Spring MVC)向けの組み込みWebサーバーになりますが、これはUndertowWebServer
の
サブクラスとなるため、Graceful Shutdownの処理はReactiveと同じになりますね。
また、そもそもGraceful Shutdownはどのようにして呼び出されるのでしょう?
仕組みとしては、Shutdown Hookを使っているようです。
SpringApplication
で、SpringApplicationShutdownHook
をShutdown Hookとして登録します。
SpringApplicationShutdownHook
では、JavaVMの停止が始まると各ApplicationContext
のクローズを順次行います。
そして、SmartLifecycle
インターフェースが実装されているものに関しては、SmartLifecycle#stop
が呼び出されます。
あとはWebServer
インターフェースを介して、Graceful Shutdownが行われるという仕掛けです。
もちろん、Graceful Shutdownが行われるのはserver.shutdown
プロパティがgraceful
に設定されていれば、ですが。
これは、各WebServer
の実装が判定します。たとえば、Tomcatであれば以下になります。
これで、およそGraceful Shutdownの流れはわかったかなと思います。
まとめ
Spring Boot 2.3で追加されたGraceful Shutdownですが、だいぶ時間が経ってからにはなりましたが自分でも確認して
みました。
わかりやすい挙動だったのと、実装箇所もちゃんと見れたのでよかったかなと思います。