すべてのプロダクト
Search
ドキュメントセンター

Microservices Engine:MSE XXL-JOB を使用したグレースフルシャットダウンの実装

最終更新日:Mar 11, 2026

ローリングデプロイやアプリケーションの再起動中に、実行中の XXL-JOB タスクが途中で中断され、ビジネスデータの不整合やスケジューリングの失敗を引き起こす可能性があります。Microservices Engine (MSE) XXL-JOB は、SchedulerX プラグインを通じて組み込みのグレースフルシャットダウン機能を提供します。この機能は、エグゼキュータからトラフィックをドレインし、実行中のジョブが完了するのを待ってからプロセスを停止します。オープンソースの XXL-JOB サーバーコードを変更する必要はありません。

仕組み

グレースフルシャットダウンが有効な場合、MSE XXL-JOB はエグゼキュータを停止する前に、次のシーケンスに従います。

  1. エグゼキュータの登録解除 -- エグゼキュータは registryRemove リクエストを MSE XXL-JOB サーバーに送信し、自身をアクティブなエグゼキュータリストから即座に削除します。サーバーは address_listxxl_job_group テーブルでリアルタイムに更新するため、新しいジョブはこのノードにディスパッチされません。

  2. 実行中のジョブの待機 -- シャットダウンモードに応じて、エグゼキュータは実行中のすべてのジョブ (およびオプションで JobThread キュー内のキューイングされたジョブ) が完了するのを待ちます。

  3. 実行結果の報告 -- TriggerCallbackThread は、保留中のすべての実行結果をサーバーにフラッシュします。

  4. プロセスの停止 -- JVM シャットダウンフックが完了し、アプリケーションプロセスが終了します。

オープンソース XXL-JOB の機能拡張が必要な理由

オープンソースの XXL-JOB には、クリーンなシャットダウンを妨げる 2 つの問題があります。

問題根本原因影響
オフラインノードにディスパッチされるジョブxxl_job_group テーブルの address_list は、JobRegistryHelper によって定期的に更新され、リアルタイムではありません。エグゼキュータが xxl_job_registry から登録解除された後も、トリガーは古い address_list を読み取ってしまいます。デプロイ中のスケジューリング失敗。
実行中のジョブの強制終了エグゼキュータが停止すると、XxlJobExecutor#destroyremoveJobThread を呼び出し、JobThread を即座に中断してキュー内のすべてのリクエストを破棄します。データの不整合とジョブ実行記録の失敗。

オープンソース XXL-JOB の内部ジョブ処理フロー

オープンソース XXL-JOB におけるジョブのディストリビューションと実行プロセスには、XXL-JOB AdminXXL-Job Executor の 2 つのモジュールが関わります。

エグゼキュータの登録:

  1. XXL-JOB SDK が起動すると、ビジネスアプリケーションは ExecutorRegistryThread スレッドを初期化し、XXL-JOB Admin に継続的にハートビートメッセージを送信します。

  2. ハートビートメッセージを受信すると、XXL-JOB Admin は JobRegistryHelper を通じてエグゼキュータ情報を xxl_job_registry データベーステーブルに書き込みます。

  3. JobRegistryHelper 内のスレッドは、登録済みエグゼキュータのリストを提供する xxl_job_group テーブルの address_list フィールドを定期的にクエリして更新します。

imageimage

オンラインエグゼキュータの選択:

  1. スケジューリングスレッドがジョブをトリガーすると、XxlJobTrigger がジョブを実行します。

  2. ジョブを実行する前に、XxlJobTriggerxxl_job_group テーブルの address_list フィールドからエグゼキュータのリストを読み取ります。

  3. ExecutorRouter は、指定されたルーティングポリシーに基づいてリストからエグゼキュータを選択します。

  4. XxlJobTrigger は RPC リクエストを送信し、選択されたノードにジョブをディスパッチします。選択されたノードがオフラインの場合、ジョブのトリガーは失敗します。

image

ジョブの実行と結果のフィードバック:

  1. エグゼキュータがジョブリクエストを受信すると、ジョブ ID に基づいてジョブごとに JobThread スレッドを作成します。

  2. ジョブリクエストがトリガーされると、現在の JobThread スレッドが処理するためにキューに追加されます。ジョブごとにブロッキングポリシーは異なります。

  3. JobThread スレッドは、キュー内のトリガー結果を継続的に読み取り、対応する JobHandler を実行してビジネスロジック処理を完了します。

  4. ジョブが終了すると、JobThread スレッドは実行情報を TriggerCallbackThread の実行応答キューに送信し、次のジョブに進みます。

  5. エグゼキュータが停止すると、XxlJobExecutor.destroy メソッドを実行して JobThread スレッドを停止し、スケジューリングリクエストのキューをクリアします。

TriggerCallbackThread スレッドは継続的に実行され、現在の実行結果キューをロードし、それらをバッチで XXL-JOB Admin にディスパッチします。結果の送信に失敗した場合は、ローカルディスクに保存して後で再試行します。

image

オープンソース XXL-JOB でのグレースフルシャットダウンの実装

MSE SchedulerX プラグインなしでオープンソース XXL-JOB を使用する場合、トラフィックの削除、キュー内のジョブの完了待機、そしてアプリケーションのシャットダウンという 3 つのステップに従うことで、手動でグレースフルシャットダウンを実装できます。

com.xxl.job.core.executor.XxlJobExecutor#destroy メソッドは、Spring Boot モードでアプリケーションプロセスが終了する際に自動的にコールバックを実行します。しかし、デフォルトのロジックではグレースフルシャットダウンが完全には実装されていません。以下の変更が必要です。

ステップ 1: アプリケーションノードからのトラフィックの削除

XxlJobExecutor#destroystopEmbedServer() メソッドは、ハートビート登録メカニズムを停止させ、XXL-JOB Admin に registryRemove リクエストを送信することで、現在のエグゼキュータを削除します。しかし、xxl_job_group テーブルの address_list フィールドはリアルタイムで同期されないため、トラフィックは効果的に削除されません。

これを修正するには、次のいずれかの方法で XXL-JOB Admin サーバーを変更します。

  • JobRegistryHelper.registryRemove メソッドに処理ロジックを追加して、xxl_job_group テーブルの address_list フィールドを更新します。freshGroupRegistryInfo メソッドで更新ロジックを実装することもできます。

  • XxlJobTrigger#trigger() メソッドを変更して、自動登録プロセス中に xxl_job_registry テーブルから直接 address_list を読み取るようにします。

ステップ 2: キュー内のジョブの完了待機

XxlJobExecutor#destroy メソッドを変更して、キュー内のすべてのジョブが完了するのを待ちます。

public void destroy(){

    // エグゼキュータサーバーを破棄
    stopEmbedServer();

    // jobThreadRepository を破棄
    if (jobThreadRepository.size() > 0) {
        List keyList = new ArrayList(jobThreadRepository.keySet());
        for (int i=0; i < keyList.size(); i++) {
            JobThread jobThread = jobThreadRepository.get(keyList.get(i));
            // キュー内のすべてのジョブが完了するのを待ちます。
            while (jobThread != null && jobThread.isRunningOrHasQueue()) {
                try {
                    TimeUnit.SECONDS.sleep(1L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    jobHandlerRepository.clear();

    // JobLogFileCleanThread を破棄
    JobLogFileCleanThread.getInstance().toStop();

    // TriggerCallbackThread を破棄
    TriggerCallbackThread.getInstance().toStop();

}

TriggerCallbackThread.getInstance().toStop() メソッドは、TriggerCallbackThread スレッドが停止した後に実行結果を同期するため、結果のフィードバックに追加の処理は必要ありません。

ステップ 3: アプリケーションプロセスの停止

アプリケーションプロセスを停止するには、アプリケーションのデプロイメントスクリプトで kill -15 を使用して JVM シャットダウンフックをトリガーします。ビジネス要件に基づいて、タイムアウト時にアプリケーションプロセスを強制的に停止することもできます。または、Spring Boot Actuator の /actuator/shutdown エンドポイントを使用して、グレースフルシャットダウン機能を統合することもできます。

前提条件

開始する前に、以下を確認してください。

次の依存関係を pom.xml に追加します。

<dependency>
  <groupId>com.aliyun.schedulerx</groupId>
  <artifactId>schedulerx3-plugin-xxljob</artifactId>
  <version>最新バージョン</version>
</dependency>

プラグインとエグゼキュータの統合

アプリケーションフレームワークに合った統合方法を選択してください。

Spring Boot (推奨)

プラグインは Spring Boot の自動構成を通じてグレースフルシャットダウンを自動的に登録します。追加のコードは不要です。

シャットダウンモードを application.properties に追加します。

xxl.job.executor.shutdownMode=WAIT_ALL

または application.yml で:

xxl:
  job:
    executor:
      shutdownMode: WAIT_ALL

Spring (Boot 以外)

Spring Web アプリケーションの場合、上記の Spring Boot セクションの Maven 依存関係と application.properties 構成を追加し、次にプラグインイニシャライザを web.xml に登録します。

<web-app>
  <context-param>
    <param-name>globalInitializerClasses</param-name>
    <param-value>com.aliyun.schedulerx.xxljob.enhance.XxlJobExecutorEnhancerInitializer</param-value>
  </context-param>
</web-app>

フレームワークなし (プレーン Java)

フレームワークなしでエグゼキュータを起動するアプリケーションの場合、プラグインの拡張機能を手動でロードし、JVM シャットダウンフックを登録します。

サンプルコード

public static void main(String[] args) {
    try {
        // エグゼキュータ構成をロード
        Properties xxlJobProp = FrameLessXxlJobConfig.loadProperties("xxl-job-executor.properties");

        // SchedulerX グレースフルシャットダウン拡張機能をロード
        EnhancerLoader.load(xxlJobProp);

        // エグゼキュータを起動
        FrameLessXxlJobConfig.getInstance().initXxlJobExecutor(xxlJobProp);

        // kill -15 がグレースフルシャットダウンをトリガーするようにシャットダウンフックを登録
        Runtime.getRuntime().addShutdownHook(new Thread(() ->
            FrameLessXxlJobConfig.getInstance().destroyXxlJobExecutor()
        ));

        // メインスレッドをブロック
        while (true) {
            try {
                TimeUnit.HOURS.sleep(1);
            } catch (InterruptedException e) {
                break;
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    } finally {
        FrameLessXxlJobConfig.getInstance().destroyXxlJobExecutor();
    }
}

xxl-job-executor.properties にシャットダウンモードが含まれていることを確認してください。

xxl.job.executor.shutdownMode=WAIT_ALL

アプリケーションのグレースフルストップ

シャットダウンモードは、JVM が SIGTERM シグナル (kill -15 に相当) を受信した場合にのみ有効になります。ご利用のデプロイ環境に合った方法を選択してください。

説明

Spring Boot アプリケーションで Spring Boot Actuator が有効になっている場合、/actuator/shutdown エンドポイントを通じてグレースフルシャットダウンをトリガーすることもできます。

セルフマネージド CD パイプライン

SIGTERM を送信し、アプリケーションが終了するのを待ち、タイムアウト時に SIGKILL にフォールバックする stop.sh スクリプトを作成します。

アプリケーションシャットダウンスクリプトのサンプル:

# アプリケーション起動時に書き込まれる PID ファイルへのパス
PID="{アプリケーションのデプロイパス}/app.pid"
FORCE=1
if [ -f ${PID} ]; then
  TARGET_PID=`cat ${PID}`
  kill -15 ${TARGET_PID}
  loop=1
  while(( $loop<=5 ))
  do
    ## 独自のヘルスチェックロジックに置き換えてください
    health
    if [ $?  == 0 ]; then
      echo "check $loop times, current app has not stop yet."
      sleep 5s
      let "loop++"
    else
      FORCE=0
      break
    fi
  done
  if [ $FORCE -eq 1 ]; then
    echo "App(pid:${TARGET_PID}) stop timeout, forced termination."
    kill -9 ${TARGET_PID}
  fi
  rm -rf ${PID}
  echo "App(pid:${TARGET_PID}) stopped successful."
fi

Kubernetes

Kubernetes はデフォルトでコンテナ内の PID 1 に SIGTERM を送信し、これにより JVM シャットダウンフックが自動的にトリガーされます。

アプリケーションが PID 1 でない場合 (例えば、マルチプロセスコンテナ内)、preStop フックを構成してシグナルを明示的に送信します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      # デフォルト: 30。予想される最大ジョブ実行時間よりも長い値を設定します。
      terminationGracePeriodSeconds: 30
      containers:
      - name: my-app-container
        image: my-app-image:latest
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "kill -15 <app-pid> && sleep 30"]
重要

terminationGracePeriodSeconds には、予想される最大ジョブ実行時間よりも長い値を設定してください。猶予期間がジョブの完了前に終了すると、Kubernetes は SIGKILL を送信して Pod を強制終了させるため、グレースフルシャットダウンが無効になります。デフォルト値は 30 秒です。

Alibaba Cloud アプリケーションリリースプラットフォーム

Alibaba Cloud アプリケーションリリースプラットフォーム向けの自動グレースフルシャットダウン統合は、まもなく利用可能になります。

シャットダウンモードのリファレンス

モード動作使用場面
WAIT_ALL (推奨)終了する前に、実行中のすべてのジョブおよびキュー内のジョブが完了するのを待ちます。ほとんどの本番ワークロード。ジョブは失われません。
WAIT_RUNNING現在実行中のジョブが完了するのを待ちます。キュー内のジョブは破棄されます。キュー内のジョブよりも高速な再起動が優先される、レイテンシーに敏感なデプロイメント。
未構成デフォルトのオープンソース XXL-JOB の動作を使用します。実行中のジョブは中断され、キュー内のジョブは破棄されます。本番環境では推奨されません。

アプリケーションプロパティでシャットダウンモードを構成します。

# グレースフルシャットダウンモード。有効な値: WAIT_ALL、WAIT_RUNNING。
# 未構成の場合、デフォルトのオープンソース XXL-JOB の動作が適用されます (グレースフルシャットダウンなし)。
xxl.job.executor.shutdownMode=WAIT_ALL

または application.yml で:

xxl:
  job:
    executor:
      # グレースフルシャットダウンモード。有効な値: WAIT_ALL、WAIT_RUNNING。
      shutdownMode: WAIT_ALL

関連ドキュメント