TL;DL
時間がかかるバッチ処理をECSで運用する場合は、SIM_TERM発火から30秒までに安全に終了させる考慮が必要。
(AWS Batchなどを使った方が、サーバー停止考慮しなくて楽かもしれません)
課題
Laravelをコンテナを利用した環境で運用する場合に、コンテナ入れ替え時に長時間かかる処理が動いている場合に途中でコンテナが停止してしまい、予期せず処理が中断してしまう場合があります。
私の経験としてAWS ECSでの運用を例にして説明します。
(内容には間違いを含む可能性があります)
タスクが停止すると、各コンテナのエントリプロセス (通常は PID 1) に SIGTERM シグナルを送信します。タイムアウトが経過すると、今度は SIGKILL シグナルをプロセスに送信します。デフォルトでは、SIGTERM シグナルの送信後 30 秒のタイムアウトで SIGKILL シグナルを送信します。この値は、ECS タスクのパラメータの stopTimeout を更新することによってタスクのコンテナ単位で調整するか、ECS エージェントの環境変数
supervisorの起動をentrypoint.shで起動している場合、ecsからsupervisorに向けてSIG_TERMがシグナリングされます。
exec /usr/bin/supervisord -c /etc/supervisord.conf
supervisorがシグナルを受け取ると、supervisorにて起動されたすべての子プロセスにシグナリングされ、すべての子プロセスが完了するとsupervisorが停止します。
phpでのシグナリング処理
pcntl_signalを利用してシグナルハンドラを登録します
https://www.php.net/manual/ja/function.pcntl-signal.php#refsect1-function.pcntl-signal-examples
Laravelのシグナリング処理
アプリケーションサーバーの場合
基本的にユーザーへレスポンスをすぐに返すはずなので、考慮不要
Workerの場合
Laravelがすでにpcntl_signalでハンドラを設定済みなので、Laravel側の変数値を参照する
https://github.com/laravel/framework/blob/12.x/src/Illuminate/Queue/Worker.php#L794
※注意:pcntl_signal() は、既存のシグナルハンドラがある場合にはそれを上書きします。
→ https://www.php.net/manual/ja/function.pcntl-signal.php#refsect1-function.pcntl-signal-notes
Schedulerの場合
Laravel側は利用していないので、自分でシグナルハンドラを登録する
どうするか
worker
キューワーカーで処理する場合、LaravelがSIG_TERMのハンドラを設定済みなので、上書きしないようにしなければいけません。
app('queue.worker')->shouldQuit の値を見ることで、LaravelがSIG_TERMを検知したかどうかを判定することが出来ます
<?php
declare(strict_types=1);
namespace App\Exceptions;
use RuntimeException;
/**
* 実行中に SIGTERM シグナルを受け取りました
*/
class ReceivedSigTermException extends RuntimeException
{
}
class SampleJob implements ShouldQueue
{
public function __construct(public int $startWithId)
{
}
public function handle(): void
{
$list = TargetList::where('id', '<=', $this->startWithId)->limit(100)->get();
try {
foreach ($list as $one) {
$this->checkSigTerm();
try {
Log::debug("実行中: {$one->id}");
// 10秒程度のかかる処理
$this->checkSigTerm();
// 10秒程度のかかる処理
$this->checkSigTerm();
// 10秒程度のかかる処理
Log::debug("完了: {$one->id}");
} catch (ReceivedSigTermException $e) {
Log::warning("SIGTERMを検知したので処理を中断しました: {$one->id}");
// 新しいJobキューを発行して一旦Jobを完了する
self::dispatch($one->id)
->onQueue($this->job->getQueue())
->delay(1);
throw $e;
}
}
} catch (ReceivedSigTermException) {
Log::warning('SIGTERMを検知したので処理を中断しました');
}
}
/**
* @return void
* @throws ReceivedSigTermException
*/
public function checkSigTerm(): void
{
// Laravelが`SIGTERM`を検知するとtrueに設定される
if (optional(app('queue.worker'))->shouldQuit) {
throw new ReceivedSigTermException();
}
}
}
scheduler(cron)
あとで書きます