Laravel Worker が ECS の Scale In でエラーを吐かないようにする
Laravel Worker が ECS の Scale In でエラーを吐かないようにする

Laravel Worker が ECS の Scale In でエラーを吐かないようにする

はじめに

DROBE では外部のサービスを利用したメールの送信など、時間の掛かる処理を行う際に Laravel の Worker を多用しています。

この記事では、ECS Fargate を Worker として使用した際に安定稼働させるための Tips を共有します。

Worker 周りの構成

DROBE では Laravel の Worker が使う Queue driver に AWS の SQS を採用しています。

Worker は ECS Fargate を利用していて、CloudWatch が SQS の詰まり具合を監視し、詰まり具合に応じて Worker Service を通して Worker コンテナの Scale Out / In を行っています。

DROBE の Worker 周りの構成
DROBE の Worker 周りの構成

この構成にしておく事で、Queue に Job が大量に積まれると自動で Worker が増えて Job の並列数があがり、Job が減ってくると自動で Worker を減らす事が出来ます。

例えばメール通知を Job でやっている場合に特定の時間にメール送信が集中するといった場合などに有効な構成だと思っています。

Scale In の課題

この構成において Scale out に関しては特に問題などなく運用が可能なのですが、 Scale in に関しては気をつけないと Job を処理中の Worker が途中で死んでしまいます。

何らかの要因で ECS Task に終了の命令が出されると ECS Task は一定時間後に終了します。命令が出されてから終了するまでの時間はデフォルトで 30 秒ですが、ECS の Task 定義の設定によって 120 秒まで伸ばす事が出来ます。

ここで Laravel の以下のコマンドで起動したプロセスは SIGTERM を受信した後も job を dequeue しようとしてしまいます。

php artisan queue:work

つまり、以下の図のように Scale in のタイミングによっては Worker が job を処理している途中でプロセスが終了する事になってしまいます。

ECS で Laravel Worker を動かしていると Scale in で job が失敗する可能性がある
ECS で Laravel Worker を動かしていると Scale in で job が失敗する可能性がある

結果として不定期でこのようなエラーが鳴り響く、という事態となってしまいます。

sentry に記録されたエラー
sentry に記録されたエラー

弊社では Worker で行う内容は基本的に冪等性を担保して作っているので必要があれば再実行すれば良いのですが、精神衛生上とってもよろしくない状況となってしまっていました。

対策

この問題に対応するためには、SIGTERM が来たら job を dequeue しないようにしてあげる必要があります。

この記事を参考に、worker 用の entry point となるスクリプトを準備しました。

#!/bin/bash

exit_trap(){
  echo "received SIGTERM, exiting..."
  exit 0
}

trap exit_trap SIGTERM

while true
do
  php artisan queue:work --tries=1 --once
done
worker-entrypoint.sh

ここでのポイントは --once フラグです。このフラグをつける事で worker process は 1 つの job の実行が終わったら終了します。このスクリプトでは job を終了して worker process が終了したら再度実行するように while loop で囲っています。

SIGTERM を受け取ったら exit_trap により、それ以上 worker process を生成せずに終了します。

ただしこのスクリプトについては、以下の部分については注意が必要なのでご注意ください。

  1. worker が 1 つの job の処理に 120 秒以上かけているものがあればエラーは発生してしまう可能性がある
  2. worker process は job を処理する事に起動されるので、フレームワークの bootstrap 分の処理が process を建てっぱなしにしておくよりも余計にかかる。(ただしそのおかげで逆にメモリーリークなどは気にしなくて良くなる)

動作のイメージとしてはこのようになります。

worker-entrypoint.sh と SIGTERM
worker-entrypoint.sh と SIGTERM

弊社ではこのスクリプトを worker-entrypoint.sh と命名して、ECS の Command として叩いています。

終わりに

Laravel の Worker を ECS Fargate で運用する際の tips について記載しました。

この問題は Worker の Auto scale を導入して以来頭を悩ませていたのですが、スッキリと解決されて嬉しくなって記事化しました。

最後に、DROBE では一緒にコードを書く仲間を募集しています!

少しでも気になった方は気軽に 応募、DM などでお知らせください!

参考資料