🌟

AWS SQSとSpringを組み合わせたアプリのGraceful Shutdown戦略

2022/11/06に公開

仕事上で出くわした際、検索すると日本語の情報が少なかったため備忘録として残しておきます。

はじめに

アプリケーション間の連携方式を考える際、AWSのSQSを間に挟むことで疎結合な設計を行う人も多いかと思います。さらに最近はDockerも普及していることから、メッセージを取り出すワーカーアプリケーションをECS(Fargate)で動かし、SQSのメッセージ数に応じてタスク数を増減するような実装をする方もいるかと思います。

この構成では、スケールインなどでタスクが停止した際に、メッセージが処理途中で停止され不整合のような状態になるべく陥らないように考慮する必要があります。

AWS公式では、正常にアプリケーションを終了するような情報が提供されています。
https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/
この記事を参照すると、コンテナのstopTimeoutを適切な秒数で設定し、
SIGTERM受信時からSIGKILL受信時までの期間にて、アプリケーションの終了処理を行うように推奨しています。

本題

前述した構成をSpringで実現し、タスク停止時に安全に処理を終了する方法を考えます。

SpringでSQSを定期的にポーリングする方法としては、scheduledアノテーションを使いメソッドを定期実行しながら、AWS SDKを用いてSQSからメッセージ取得する方法が考えられます。

scheduedアノテーションを用いて定期実行している際にSIGTERMを受信した場合に、アプリケーションが正常終了するかを確認します。

そこで以下のような定期実行タスクを作成します。

AnnotaionTask
@Component
public class AnnotaionTask {

    @Scheduled(fixedRate = 1000)
    public void test(){
        System.out.println("Annotaion Scheduled Task start");
        try {
            Thread.sleep(30000);
            System.out.println("Annotaion Scheduled Task end");
        } catch (InterruptedException e) {
            System.out.println("Annotaion Scheduled Task Interrupted");
            e.printStackTrace();
        }
    }
}

このアプリケーションを実行し、sleep中にkillコマンドを実行します。するとInterrupted Exceptionが発生します。

2022-11-02 22:22:18.703  INFO 24696 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
Annotaion Scheduled Task start
2022-11-02 22:22:18.730  INFO 24696 --- [           main] h.d.a.a.AwsEcrSampleApplication          : Started AwsEcrSampleApplication in 3.205 seconds (JVM running for 3.897)
Annotaion Scheduled Task end
Annotaion Scheduled Task start
Annotaion Scheduled Task Interrupted
java.lang.InterruptedException: sleep interrupted
        at java.base/java.lang.Thread.sleep(Native Method)

SIGTERM受信時のデフォルトの挙動は即時終了となっているようです。
アプリケーションを正常に終了させるには、SIGTERM受信時に定期実行タスクが終了するのを待つようにSpringの挙動を設定する必要があります。

ここでSpringのドキュメントを読むと、プロパティの項目に以下のものがあります。
https://spring.pleiades.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.core.spring.task.scheduling.shutdown.await-termination-period

spring.task.scheduling.shutdown.await-termination
シャットダウン時にスケジュールされたタスクが完了するまでエグゼキューターが待機するかどうか。
spring.task.scheduling.shutdown.await-termination-period
残りのタスクが完了するまでエグゼキューターが待機する最大時間。

この項目を設定することで即時終了しないように設定可能なように読み取れます。

ところがこの設定を実施しても、先ほどと同様にSIGTERM受信時に定期実行タスクが終了してしまいます。

色々調べたのですが、これはSpringのバグではないかというIssueが挙げられていました。
https://github.com/spring-projects/spring-framework/issues/26482
修正にむけてプルリクが出されていますが、まだ取り込まれていないようです(2022/11/06現在)。

そのため現時点では、SIGTERM受信時に定期実行タスクが終了するのを待つようにするためには、scheduledアノテーションを使わずにタスクを登録する必要が出てきます。

具体的な方法はいくつか考えられますが、ここではSchedulingConfigurerを継承したクラスを作成し、定期実行タスクの登録を行う方法を紹介します。

先ほどのタスクからscheduledアノテーションを外したものを用意します(コード例では別なクラスとして作成してますが、元々のクラスから直接アノテーションを外したもので構いません)。

ConfigurerSampleTask
@Component
public class ConfigurerSampleTask {
    public void test() {
        System.out.println("Configurer Scheduled Task start");
        try {
            Thread.sleep(30000);
            System.out.println("Configurer Scheduled Task end");
        } catch (InterruptedException e) {
            System.out.println("Configurer Scheduled Task Interrupted");
            e.printStackTrace();
        }
    }
}

次にSchedulingConfigurerを継承したConfigurationを作成します。

SampleSchedulingConfigurer
@Configuration
@RequiredArgsConstructor
public class SampleSchedulingConfigurer implements SchedulingConfigurer {
    private final ConfigurerSampleTask configurerSampleTask;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setTaskScheduler(threadPoolTaskScheduler());
        taskRegistrar.addFixedDelayTask(
                ()->{
                    configurerSampleTask.test();
                },
                1000
        );
    }

    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler(){
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        taskScheduler.setAwaitTerminationSeconds(60);
        return taskScheduler;
    }
}

configureTasksメソッドに渡されてきたScheduledTaskRegistrarに
TaskSchedulerの登録、および定期実行タスクの登録を行うことで、
アノテーションを用いずに定期実行タスクをスケジュール登録することができます。

またThreadPoolTaskSchedulerを別途定義する必要があるため、
threadPoolTaskSchedulerメソッドでsetWaitForTasksToCompleteOnShutdownおよびsetAwaitTerminationSecondsの設定を行ったThreadPoolTaskSchedulerを作成しBeanとして登録します(前述で紹介したプロパティ項目と同様の設定です)。

この方法は、前述のIssueが解決した場合に、scheduledアノテーションの付与とConfigurationの削除、spring.task.scheduling.shutdown.await-terminationおよびspring.task.scheduling.shutdown.await-termination-periodプロパティの設定で、scheduledアノテーションを用いた方法に戻すことができるため手間は少ないのかなと思います。

実行をしてみたところ以下のように正常に定期実行タスクが終了したのちSpringが終了しました。
うまく動作しているようです。

2022-11-02 22:31:40.842  INFO 24807 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
Configurer Scheduled Task start
2022-11-02 22:31:40.859  INFO 24807 --- [           main] h.d.a.a.AwsEcrSampleApplication          : Started AwsEcrSampleApplication in 3.164 seconds (JVM running for 3.757)
Configurer Scheduled Task end
Configurer Scheduled Task start
Configurer Scheduled Task end

まとめ

本記事では、Srpingのscheduledアノテーションで作成した定期実行タスクはSIGTERM受信で即時終了してしまい特定の環境下では相性が悪いため、SchedulingConfigurerでアノテーションを用いずに定期実行タスクを登録する方法を紹介しました。同様の事象でお困りの方に少しでも役に立てたら幸いです。

Discussion