これは、なにをしたく書いたもの?
前に、Spring BootのGraceful Shutdownを試してみました。
Spring BootのGraceful Shutdownを試す - CLOVER🍀
Quarkusでも同様のことができるみたいなので、試してみました。
Graceful Shutdown
QuarkusのGraceful Shutdownは、こちらに記載があります。
Application Initialization and Termination / Graceful Shutdown
アプリケーションを終了させようとした時に、実行しているリクエストがあれば指定されたタイムアウトまで
待つことができます。
Graceful Shutdownはデフォルトで無効になっていて、quarkus.shutdown.timeoutを設定することで有効になります。
All Configuration Properties / quarkus.shutdown.timeout
無効になっている場合は、アプリケーションは即座時に終了します。
有効にした時の注意点としては、リクエストを追跡するため若干パフォーマンスにペナルティが発生することのようです。
タイムアウトの時間の単位は、java.time.Durationの形式で指定できます。10sとかですね。
では、こちらを使っていってみましょう。
環境
今回の環境は、こちらです。
$ 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"
プロジェクトを作成する
まずはプロジェクトを作成します。extensionは、RESTEasy Reactiveのみにしておきましょう。
$ mvn io.quarkus.platform:quarkus-maven-plugin:2.3.0.Final:create \
-DprojectGroupId=org.littlewings \
-DprojectArtifactId=resteasy-graceful-shutdown \
-DprojectVersion=0.0.1-SNAPSHOT \
-Dextensions="resteasy-reactive"
作成時の情報。
[INFO] --- quarkus-maven-plugin:2.3.0.Final:create (default-cli) @ standalone-pom --- [INFO] Looking for the newly published extensions in registry.quarkus.io [INFO] ----------- [INFO] selected extensions: - io.quarkus:quarkus-resteasy-reactive [INFO] applying codestarts... [INFO] 📚 java 🔨 maven 📦 quarkus 📝 config-properties 🔧 dockerfiles 🔧 maven-wrapper 🚀 resteasy-reactive-codestart
プロジェクト内に移動。
$ cd resteasy-graceful-shutdown
生成されたソースコードは、いったん削除しておきます。
$ rm src/main/java/org/littlewings/ReactiveGreetingResource.java src/test/java/org/littlewings/*.java
JAX-RSリソースクラスを作成する
動作確認用の、JAX-RSリソースクラスを作成します。
src/main/java/org/littlewings/quarkus/resteasy/HelloResource.java
package org.littlewings.quarkus.resteasy; import java.time.Duration; import java.util.Optional; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import io.smallrye.mutiny.Uni; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.RestQuery; @Path("hello") public class HelloResource { Logger logger = Logger.getLogger(HelloResource.class); @GET @Produces(MediaType.TEXT_PLAIN) public Uni<String> message(@RestQuery Optional<Integer> sleepTime) { int s = sleepTime.filter(v -> v > 0).orElse(10); logger.infof("accept, sleep time = %d sec", s); return Uni .createFrom() .item("Hello World!!") .onItem() .delayIt() .by(Duration.ofSeconds(s)); } }
/helloでHello World!!を返しつつ、スリープする実装にしています。スリープする時間はデフォルトで10秒で、
QueryStringでも指定できるようにしています。
スリープ、と書きましたが、SmallRye Mutinyで遅延応答させる方法はこちらを参考にしています。
Graceful Shutdown無効で確認する
まずは、この状態でパッケージング。
$ mvn package
起動。
$ java -jar target/quarkus-app/quarkus-run.jar
確認。
$ time curl localhost:8080/hello Hello World!! real 0m10.023s user 0m0.012s sys 0m0.000s
レスポンスが返ってくるまでに、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 0m1.514s user 0m0.006s sys 0m0.005s
また、アプリケーションは即座に停止します。
2021-10-18 23:39:38,257 INFO [io.quarkus] (Shutdown thread) resteasy-graceful-shutdown stopped in 0.039s
Graceful Shutdownを有効にする
では、Graceful Shutdownを有効にしてみます。application.propertiesに、quarkus.shutdown.timeoutを追加します。
src/main/resources/application.properties
quarkus.shutdown.timeout=15s
quarkus.shutdown.timeoutにデフォルト値はありません。今回は、15秒に指定しました。
パッケージングして
$ mvn package
起動。
$ java -jar target/quarkus-app/quarkus-run.jar
とりあえず、確認。
$ time curl localhost:8080/hello Hello World!! real 0m10.032s user 0m0.010s sys 0m0.007s
次に、リクエストを投げて
$ time curl localhost:8080/hello
kill。
$ kill `ps -ef | grep 'java -jar' | grep -v grep | awk '{print $2}'`
すると、アプリケーション側ではGraceful Shutdownが始まります。
2021-10-18 23:58:57,575 INFO [io.qua.ver.htt.run.fil.GracefulShutdownFilter] (Shutdown thread) Waiting for HTTP requests to complete
curlには、レスポンスが返ってきます。
$ time curl localhost:8080/hello Hello World!! real 0m10.019s user 0m0.008s sys 0m0.005s
そして、アプリケーションが停止します。
2021-10-18 23:59:04,977 INFO [io.qua.ver.htt.run.fil.GracefulShutdownFilter] (vert.x-eventloop-thread-13) All HTTP requests complete 2021-10-18 23:59:05,017 INFO [io.quarkus] (Shutdown thread) resteasy-graceful-shutdown stopped in 7.443s
これで、Graceful Shutdownで現在処理しているリクエストを待ってから、アプリケーションが停止することを
確認できました。
次は、Graceful Shutdownのタイムアウトまでに処理が終わらないリクエストも投げてみます。
アプリケーションを起動。
$ java -jar target/quarkus-app/quarkus-run.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-19 00:07:18,711 INFO [io.qua.ver.htt.run.fil.GracefulShutdownFilter] (Shutdown thread) Waiting for HTTP requests to complete
10秒スリープの方は、レスポンスが返ってきます。
$ time curl localhost:8080/hello Hello World!! real 0m10.173s user 0m0.005s sys 0m0.005s
30秒スリープさせる方は待ちきれず、アプリケーション側がシャットダウンします。
2021-10-19 00:07:33,713 ERROR [io.qua.run.shu.ShutdownRecorder] (Shutdown thread) Timed out waiting for graceful shutdown, shutting down anyway. 2021-10-19 00:07:33,729 INFO [io.qua.ver.htt.run.fil.GracefulShutdownFilter] (vert.x-eventloop-thread-5) All HTTP requests complete 2021-10-19 00:07:33,759 INFO [io.quarkus] (Shutdown thread) resteasy-graceful-shutdown stopped in 15.048s
タイムアウトした、とログに書かれていますね。
クライアントにも、レスポンスは返りません。
$ time curl localhost:8080/hello?sleepTime=30 curl: (52) Empty reply from server real 0m16.793s user 0m0.011s sys 0m0.002s
最後に、アプリケーションの停止が始まった後にリクエストを投げてみましょう。
まずは起動。
$ java -jar target/quarkus-app/quarkus-run.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-19 00:23:36,794 INFO [io.qua.ver.htt.run.fil.GracefulShutdownFilter] (Shutdown thread) Waiting for HTTP requests to complete
この時、追加でリクエストを投げると、HTTPステータスコード503が返ることが確認できます。
$ time curl -i localhost:8080/hello HTTP/1.1 503 Service Unavailable connection: close content-length: 0 real 0m0.023s user 0m0.004s sys 0m0.009s
Spring Bootでは、Tomcat、Jetty、Reactor Nettyを使っている場合はネットワークレベルで受け付けを停止し、
Undertowを使っている場合は503を返すという話だったので、Undertowを使っている時と同じ挙動になっていることに
なりますね。
もっとも、Quarkusの場合は使っているのはVert.xですが。
あとは、ここまでと同じですね。Graceful Shutdownのタイムアウト時間内に終わるものはレスポンスが得られますし、
$ time curl localhost:8080/hello Hello World!! real 0m10.129s user 0m0.009s sys 0m0.010s
タイムアウトを待ちきれないものは、アプリケーションが停止後に
2021-10-19 00:23:51,795 ERROR [io.qua.run.shu.ShutdownRecorder] (Shutdown thread) Timed out waiting for graceful shutdown, shutting down anyway. 2021-10-19 00:23:51,814 INFO [io.qua.ver.htt.run.fil.GracefulShutdownFilter] (vert.x-eventloop-thread-0) All HTTP requests complete 2021-10-19 00:23:51,834 INFO [io.quarkus] (Shutdown thread) resteasy-graceful-shutdown stopped in 15.041s
応答が得られなかったことを確認できます。
$ time curl localhost:8080/hello?sleepTime=30 curl: (52) Empty reply from server real 0m17.131s user 0m0.006s sys 0m0.005s
これで、QuarkusのGraceful Shutdownの動きは確認できたかな、と思います。
実装まわりについて
少し、QuarkusのGraceful Shutdownの実装まわりを見ておきましょう。
Graceful Shutdownを有効にするためのプロパティ。
このプロパティに値を指定しておくと、GracefulShutdownFilterが追加されます。
こちらですね。
GracefulShutdownFilterはShutdownListenerの実装であり、これはシャットダウンプロセスを制御するための
リスナーです。
タイムアウトはどこで管理しているかというと、こちらになります。
この処理は、アプリケーションの停止時に呼び出されます。
停止時に呼び出される仕組みは、ShutdownHookになります。
まとめ
QuarkusのGraceful Shutdownを試してみました。
Spring Bootの時と同様、簡単に使えて、動きもわかりやすかったですね。