CLOVER🍀

That was when it all began.

QuarkusのGraceful Shutdownを試す

これは、なにをしたく書いたもの?

前に、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とかですね。

Duratin#parse

では、こちらを使っていってみましょう。

環境

今回の環境は、こちらです。

$ 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で遅延応答させる方法はこちらを参考にしています。

How to delay events?

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を有効にするためのプロパティ。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownConfig.java

このプロパティに値を指定しておくと、GracefulShutdownFilterが追加されます。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java#L400-L404

こちらですね。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/filters/GracefulShutdownFilter.java

GracefulShutdownFilterはShutdownListenerの実装であり、これはシャットダウンプロセスを制御するための
リスナーです。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownListener.java

タイムアウトはどこで管理しているかというと、こちらになります。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/core/runtime/src/main/java/io/quarkus/runtime/shutdown/ShutdownRecorder.java#L39-L41

この処理は、アプリケーションの停止時に呼び出されます。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/core/runtime/src/main/java/io/quarkus/runtime/Application.java#L202

停止時に呼び出される仕組みは、ShutdownHookになります。

https://github.com/quarkusio/quarkus/blob/2.3.0.Final/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java#L401

まとめ

QuarkusのGraceful Shutdownを試してみました。

Spring Bootの時と同様、簡単に使えて、動きもわかりやすかったですね。