Help us understand the problem. What is going on with this article?

【Laravel】 Cron タスクスケジューラの onOneServer() と withoutOverlapping() の違い

はじめに

Laravel には Cron を利用したタスクスケジューリング機能があります。

ところが素でこれをそのまま使うと,ワーカーとして複数インスタンスを立ててスケーリングしている場合などに,重複実行されてしまう問題があります。これを防ぐためには基本は onOneServer() を使っておけばいいのですが,なにやら withoutOverlapping() というものも存在することを知りました。微妙に用途が異なるみたいなので,ここで整理しておこうと思います。

現在時刻が「$H$ 時 $(i - 1)$ 分 $59$ 秒」であるとします。ここから1秒経過して「$H$ 時 $i$ 分 $0$ 秒」になった瞬間のことを考えます。

メソッド 目的
onOneServer() $command$H$ 時 $i$ 分 における実行開始 処理が既に別の場所で発生済みであるとき,ここでの実行をスキップする
withoutOverlapping() $command現在実行中 である場合, ここでの実行をスキップする

この 2 つのオプションは複合するため,以下のような 4 通りのパターンがあります。

$schedule->command($command)->everyMinute();
$schedule->command($command)->everyMinute()->onOneServer();
$schedule->command($command)->everyMinute()->withoutOverlapping();
$schedule->command($command)->everyMinute()->onOneServer()->withoutOverlapping();

図解

所要時間が 1分10秒 である $command というコマンドを考えます。これを 1 分ごと に実行します。またワーカーは 2 インスタンス 立っているとします。

それぞれ順番にタイムチャートで見てみましょう。多分,図を見れば一発で理解できると思います。

オプション無し

$schedule->command($command)->everyMinute();
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|   Time   | 0 | ... | 50 | 60 | 70 | ... | 110 | 120 | 130 | ... | 170 | 180 | 190 | ... |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Worker 1 | *********************                 *******************************        |
|          |                 *****************************                                |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Worker 2 | *********************                 *******************************        |
|          |                 *****************************                                |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+

何の工夫もしない場合です。これはおそらく意図されていない結果です。

onOneServer()

$schedule->command($command)->everyMinute()->onOneServer();
キャッシュドライバに Redis または Memcached を使っている場合
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|   Time   | 0 | ... | 50 | 60 | 70 | ... | 110 | 120 | 130 | ... | 170 | 180 | 190 | ... |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Worker 1 | *********************                 *******************************        |
|          |                 *****************************                                |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Worker 2 |                                                                              |
|          |                                                                              |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
(参考)キャッシュドライバにファイルを使っている場合
オプション無しと同じ

ほとんどの軽量な短周期バッチではこれを選択するとよいでしょう。ワーカーを水平分散したとき, 1 インスタンスだけでタスクが実行されて欲しいと考えるのは自然な思考です。

補足

すべてのインスタンスが共通のキャッシュを見る必要があるため,キャッシュドライバとして Redis または Memcached が必須 要件であることに注意してください。 構成によっては Database でも問題ないように思われますが,アトミックな読み書きが確実に保証されているのは Redis と Memcached のみです。

withoutOverlapping()

$schedule->command($command)->everyMinute()->withoutOverlapping();
キャッシュドライバにファイルを使っている場合
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|   Time   | 0 | ... | 50 | 60 | 70 | ... | 110 | 120 | 130 | ... | 170 | 180 | 190 | ... |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Worker 1 | *********************                 *******************************        |
|          |                                                                              |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Worker 2 | *********************                 *******************************        |
|          |                                                                              |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
(参考)キャッシュドライバに Redis または Memcached を使っている場合
onOneServer() + withoutOverlapping() と同じになる可能性が高いが保証はされていない

withoutOverlapping() は単体ではあまり意味のない選択肢です。これも意図されているケースは無いはずです。

補足

今回のコマンドの場合は Redis または Memcached を使っていると,後述の onOneServer() + withoutOverlapping() の動作と実質同じ動きをしてくれると思いますが,厳密には保証されていません。具体的には,以下のようなケースで失敗します。実行開始時刻にばらつきがあり,コマンドの実行時間がスケジュール周期に対して十分短い場合です。

sleep(mt_rand(0, 5));
$schedule->command('1秒で終わるコマンド')->everyMinute()->withoutOverlapping();

withoutOverlapping() は「コマンド実行中の状態が重複しないこと」しか見てくれません。例えば,Worker 2 で開始されるときに Worker 1 での実行が終了していると,重複して実行されてしまいます。その逆も然りです。

+----------+---+---+---+---+---+-----+----+----+----+----+----+
|   Time   | 0 | 1 | 2 | 3 | 4 | ... | 60 | 61 | 62 | 63 | 64 |
+----------+---+---+---+---+---+-----+----+----+----+----+----+
| Worker 1 |     *****                                 *****  |
|          |                                                  |
+----------+---+---+---+---+---+-----+----+----+----+----+----+
| Worker 2 |             *****          *****                 |
|          |                                                  |
+----------+---+---+---+---+---+-----+----+----+----+----+----+

onOneServer() + withoutOverlapping()

$schedule->command($command)->everyMinute()->onOneServer()->withoutOverlapping();
キャッシュドライバに Redis または Memcached を使っている場合
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|   Time   | 0 | ... | 50 | 60 | 70 | ... | 110 | 120 | 130 | ... | 170 | 180 | 190 | ... |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Worker 1 | *********************                 *******************************        |
|          |                                                                              |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Worker 2 |                                                                              |
|          |                                                                              |
+----------+---+-----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
キャッシュドライバにファイルを使っている場合
withoutOverlapping() と同じ

withoutOverlapping()onOneServer() と併用して,初めて意味のある選択肢になります。中程度の周期のバッチの実行時間が,突発的に肥大化したりする場合に向いている選択肢です。「タスクが同時実行されると都合が悪いかどうか」で考えるといいでしょう。

  • 「人が手作業で行ってもいい作業を,適当に設定したサイクルで回るように自動化している」 という大雑把なスケジュールの場合, onOneServer() + withoutOverlapping() が適当と言える可能性が高そうです。
  • 「毎回実行することに意味があり,たとえ前のタスクが長引いても毎回実行しなければならない」という正確なスケジュールを求めている場合, onOneServer() のみのほうが適当です。

補足

2 つを併用することで,先程問題になっていた,実行開始時刻にばらつきがあり,コマンドの実行時間がスケジュール周期に対して十分短い場合でも確実に対応することができます。

+----------+---+---+---+---+---+-----+----+----+----+----+----+
|   Time   | 0 | 1 | 2 | 3 | 4 | ... | 60 | 61 | 62 | 63 | 64 |
+----------+---+---+---+---+---+-----+----+----+----+----+----+
| Worker 1 |     *****                                 *****  |
|          |                                                  |
+----------+---+---+---+---+---+-----+----+----+----+----+----+
| Worker 2 |                                                  |
|          |                                                  |
+----------+---+---+---+---+---+-----+----+----+----+----+----+

まとめ

メソッド 目的
onOneServer() $command$H$ 時 $i$ 分 における実行開始 処理が既に別の場所で発生済みであるとき,ここでの実行をスキップする
withoutOverlapping() $command現在実行中 である場合, ここでの実行をスキップする
$schedule->command($command)->everyMinute()->onOneServer();
$schedule->command($command)->everyMinute()->onOneServer()->withoutOverlapping();
  • onOneServer() は,スケーリングを前提とするのであればすべてのコマンドに付けておくべき。また実効性を生むためには Redis か Memcached が必須。
  • withoutOverlapping() は,前のコマンドの実行が終わっていないときにスキップして欲しい場合は onOneServer() と組み合わせて付けておく。実質的には無くても問題ないケースが多い。
mpyw
PHP(Laravel) / JavaScript(React/Redux/ReactNative/Vue) / MySQL あたりが得意分野なWeb系エンジニア。最近マンネリ化がひどいので Go / Kotlin / Rust / Swift あたりから何か掘り下げたいと思っている。Go は 2.x 出てから書きます。古い記事はそのまま参考にしないようにご注意ください
http://gravatar.com/mpyw
synapse
Synapseは、オンラインサロンサービスにおけるパイオニアとして、かつて存在していたスタートアップです。
https://synapseam.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした