Spring Framework のアプリケーションを停止したときの振る舞い
Spring Framework で実装したアプリケーションを停止するときに何が起きているのか整理しました。
具体的な停止方法については Shutdown a Spring Boot Application(Baeldung) にまとまっているので参照してください。
要点
- Spring Framework のアプリケーションを安全に停止する方法が知りたい
- Spring Framework のアプリケーションに停止処理を追加する方法が知りたい
ContextClosedEvent
に対するイベントリスナーを実装するか、@PreDestroy
などで修飾したメソッドに記述する
- コンテナ実行基盤で実行している Spring Framework のアプリケーションに停止処理を追加したい
- 停止処理を実行できる時間には制限があるので気を付けましょう
停止のきっかけと振る舞い
フレームワークと関係ない Java HotSpot VM の話です。
Java HotSpot VM が停止するきっかけには内部要因と外部要因があります。
- 内部要因
daemonize
していない全てのスレッドが終了した場合- 例 -
main
スレッドとdaemonize
した複数のスレッドが存在するプログラムでmain
スレッドが終了した場合
- 例 -
System.exit()
を呼び出した場合
- 外部要因
- OS からシグナルを受信した場合
SIGINT
- Ctrl+C で実行を中断したときに発生するシグナルですSIGTERM
-kill
コマンドが通知するデフォルトのシグナルですSIGHUP
- 接続していた ptty を失った場合などに発生するシグナルです- 参考 - Oracle Solaris、LinuxおよびmacOSで使用されるシグナル
- OS からシグナルを受信した場合
どちらの場合でも、 Runtime#addShutdownHook
で登録されて待機状態になっていたスレッド(シャットダウンフック)が起動するようになっています。
(参考 - Runtime (Java SE 11 & JDK 11))
Spring Framework の登録するシャットダウンフック
ConfigurableApplicationContext を実装した抽象基底クラスの AbstractApplicationContex がシャットダウンフックを登録します。
/** * Register a shutdown hook {@linkplain Thread#getName() named} * {@code SpringContextShutdownHook} with the JVM runtime, closing this * context on JVM shutdown unless it has already been closed at that time. * <p>Delegates to {@code doClose()} for the actual closing procedure. * @see Runtime#addShutdownHook * @see ConfigurableApplicationContext#SHUTDOWN_HOOK_THREAD_NAME * @see #close() * @see #doClose() */ @Override public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) { @Override public void run() { synchronized (startupShutdownMonitor) { doClose(); } } }; Runtime.getRuntime().addShutdownHook(this.shutdownHook); } }
doClose
メソッドはアプリケーションコンテキストイベントの ContextClosedEvent
を通知します。
(つまり、アプリケーションコードに何かしら終了処理を追加したいなら ContextClosedEvent
に対するイベントリスナーを実装すればよい)
ライフサイクル管理イベントも通知する (onClose
) ので、@PreDestroy
などで修飾したメソッドも呼び出されるはずです。
protected void doClose() { // Check whether an actual close attempt is necessary... if (this.active.get() && this.closed.compareAndSet(false, true)) { if (logger.isDebugEnabled()) { logger.debug("Closing " + this); } if (!IN_NATIVE_IMAGE) { LiveBeansView.unregisterApplicationContext(this); } try { // Publish shutdown event. publishEvent(new ContextClosedEvent(this)); } catch (Throwable ex) { logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex); } // Stop all Lifecycle beans, to avoid delays during individual destruction. if (this.lifecycleProcessor != null) { try { this.lifecycleProcessor.onClose(); } catch (Throwable ex) { logger.warn("Exception thrown from LifecycleProcessor on context close", ex); } } // Destroy all cached singletons in the context's BeanFactory. destroyBeans(); // Close the state of this context itself. closeBeanFactory(); // Let subclasses do some final clean-up if they wish... onClose(); // Reset local application listeners to pre-refresh state. if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } // Switch to inactive. this.active.set(false); } }
コンテナ実行基盤ごとの停止処理と猶予時間
ここでは Docker コンテナを実行できる環境のことを コンテナ実行基盤 と呼んでいます。
停止操作を行うと、コンテナのメインプロセス(PID が 1 のプロセス) に TERM
シグナル (SIGTERM
) を通知します。
その後、所定の猶予時間を経過すると KILL
シグナル (SIGKILL
) を通知します。
KILL
シグナルはプログラムで処理できないシグナルで、対象のプロセスは OS により停止されます。
(参考 - signal(7) - Linux manual page)
つまり、アプリケーションで終了処理を実行できるのは TERM
シグナルを受信してから KILL
シグナルを受信するまでの間だけです。
コンテナ実行基盤ごとに猶予時間の初期値を整理してみました。
種類 | 猶予時間 | 変更できるかどうか |
---|---|---|
Docker Daemon | 10 秒 | コマンド引数で指定できる [1] |
Kubernetes | 30 秒 | マニフェストで設定できる [2] |
Amazon ECS | 30 秒 | コンテナエージェントの環境変数で設定できる [3] [4] |
- [1] - docker stop
- [2] - Pod の終了
- [3] - StopTask - Amazon Elastic Container Service
- [4] - Amazon ECS コンテナエージェントの設定
今のところ設定できる猶予時間に上限値は設けられていないようです。 Too big terminationGracePeriodSeconds value leads to pod hanging forever in Terminating state #84298
あまり長い時間を設定するとコンテナの特徴である軽量な停止と開始を阻害するだけなので、仕組み自体を検討したほうがいいでしょう。