はじめに
Spring Boot 2.3からGraceful shutdownが導入されました。アプリケーションプロパティを設定するだけで簡単にGraceful shutdownを実現することができますが、内部の実処理はブラックボックスになっています。このままでは気になって夜も眠れません。
そこで本記事ではJettyを例にし、ソースコードリーディングをしてわかった内部処理をコードとともに紹介していきたいと思います。
今回使用するバージョン・サーバ
Spring Boot 2.3.1を使用しています。また、サーバはJettyを使用します。
前提: Spring BootでのGraceful shutdownの挙動
ドキュメントに記載の通りですが、Graceful shutdown中の具体的な挙動としては
- 新たに来たリクエストは受け付けない
- 処理中のリクエストを捌き切るまでサーバを停止させない
です。この「新たに来たリクエストは受け付けない」というのは使用するWebサーバによって挙動が異なります。
Webサーバ | 挙動 |
---|---|
Tomcat Jetty Reactor Netty |
ネットワークレイヤーでコネクションを受け付けない |
Undertow | 503を返す |
参考: Spring Boot Reference Documentation#boot-features-graceful-shutdown
次からはこの1, 2の挙動をソースコードと照らし合わせて確認したいと思います。
JettyのGraceful shutdownの挙動をソースコードレベルで確認してみる
上記の挙動はドキュメントに記載されている内容です。この挙動をソースコードレベルで照らし合わせて確認してみたいと思います。
※注意: 今回追うコードはSpring Boot 2.3.1を前提としています。内部の挙動は今後のバージョンアップで変わる可能性があるのでご注意ください。
また、サーバはJettyを使用しているため、他サーバでは挙動が異なると思います。
JettyのGraceful shutdownの起点となる箇所
Spring Bootの起動ログなどをもとに追ってみると以下のJettyWebServerクラスのshutDownGracefullyというメソッドにたどり着きます。
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
if (this.gracefulShutdown == null) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
return;
}
this.gracefulShutdown.shutDownGracefully(callback);
}
このメソッドではthis.gracefulShutdown
インスタンスがnullかどうかによって処理が分岐しており、nullでない場合にGraceful shutdownを行うようになっています。
では、もっと踏み込んでthis.gracefulShutdown.shutDownGracefully(callback)
の内部を追っていきます。
以下メソッドはその内部になります。
大きく処理が(1)のshutdownメソッド, (2)のawaitShutdownメソッドに分かれるのでそれぞれ追ってみましょう。
void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
for (Connector connector : this.server.getConnectors()) {
shutdown(connector); // (1)
}
this.shuttingDown = true;
new Thread(() -> awaitShutdown(callback), "jetty-shutdown").start(); // (2)
}
(1)shutdownメソッド
private void shutdown(Connector connector) {
try {
connector.shutdown().get();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
catch (ExecutionException ex) {
// Continue
}
}
まず、サーバが持っているコネクタの数だけループを回しシャットダウンしていきます。これにより、リクエストを受け付けるコネクタが存在しなくなるため、Jettyは新たなリクエストを受け付けなくなります。
ちなみにコネクタがどのようにシャットダウンされていくかは上記connector.shutdown()
の実装を追っていけば分かります。途中は端折りますが、以下のようなclose処理によってチャネルが閉じられることでコネクションが切られ、リクエストを受け付けなくなります(具体的にはserverChannel.close()
の部分)。
これ以上はJettyの実装になるため本記事では割愛します。
@Override
public void close()
{
super.close();
ServerSocketChannel serverChannel = _acceptChannel;
_acceptChannel = null;
if (serverChannel != null)
{
removeBean(serverChannel);
if (serverChannel.isOpen())
{
try
{
serverChannel.close();
}
catch (IOException e)
{
LOG.warn(e);
}
}
}
_localPort = -2;
}
ここまでをまとめると、(1)では以下のことを行っていました。
- サーバが持つコネクタをシャットダウンさせることでコネクションがクローズしていき、リクエストを受け付けなくする
これはまさしく最初の前提で説明したGraceful shutdownの1番の挙動であることが分かります。
(2)awaitShutdownメソッド
つづいて(2)の処理を見ていきます。
awaitShutdownメソッドの内部は以下のようになっています。
private void awaitShutdown(GracefulShutdownCallback callback) {
while (this.shuttingDown && this.activeRequests.get() > 0) {
sleep(100);
}
this.shuttingDown = false;
long activeRequests = this.activeRequests.get();
if (activeRequests == 0) {
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
else {
logger.info(LogMessage.format("Graceful shutdown aborted with %d request(s) still active", activeRequests));
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
}
}
while文でシャットダウン中かつアクティブなリクエストが1件以上残っていれば0.1秒sleepし、それをリクエストがなくなるまで続けます。
その後アクティブなリクエストが0になったら完全にシャットダウンさせています。
これは処理中のリクエストを安全に最後まで処理する挙動であることが分かります。
これもまさしく前提で説明した2番の挙動と一致しています。
まとめ
以上でSpring BootでのGraceful shutdown(Jettyの場合)の挙動をソースコードレベルで確認できました。
最初ドキュメントを見たときにJettyの挙動としてネットワークレイヤーでコネクションを受け付けないというのがどういうことなのかいまいちぴんと来ていませんでした。
今回ソースコードを呼んでみてコネクタのクローズを行うことでコネクションを受け付けなくしているのだと分かり、ドキュメントに記載されていた内容を理解することができました。
やはり分からないときはソースコードを実際に読んでみるのが一番だと思いました。