Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Laravel Worker x SQSの組み合わせで少しハマったこと

こんにちは!SREグループの三橋です。

普段はNew Relicのことばかり書いている私ですが、本日はLaravel Worker x SQSのお話しをします。

弊社ではちょうどPHPアプリケーションのコンテナ化対応に取り組んでおり、その対応の中でLaravel WorkerとAmazon SQSを使って非同期処理を実装する必要がありました。その際に少しつまづいた部分が2点あったので、その内容についてご紹介します。
※ 本記事はLaravel 11.x前提で記載しています。バージョンによっては仕様が異なる可能性がありますのでご注意ください。

コンテナ化対応の取り組みの背景については、以前発表しているので是非こちらもご覧いただけますと幸いです!

tech.yappli.io

想定する読者

  • これからLaravelアプリケーションのコンテナ化に取り組もうと考えている方
  • Amazon SQSを愛してやまない方
  • Laravelの仕様にご興味のある方

SQSに対するLaravel Workerのリトライ実装

起きた事象: SQSの設定でVisibility Timeoutを設定しているにも関わらず、それよりも早い段階でリトライ処理が走っていた。

SQSにはメッセージの重複処理などを防ぐためVisibility Timeoutというパラメーターがあります。これを設定していると指定した時間、コンシューマー(今回の場合はLaravel Worker)からメッセージ(今回の場合ジョブ)が見えなくなるというものです。

docs.aws.amazon.com

そのためWorkerでジョブの実行に失敗した場合、Visibility Timeoutが過ぎるまでリトライは走らないものと想定していました(その間キューにジョブがないようにように見えるため)。ところがどんなにこの値を長くしてもそれより前にリトライ処理が走っていたのです。

そして色々と調査した結果下記が原因であることがわかりました。

原因: Laravel Workerがリトライの際にSQSに対し、ChangeMessageVisibility APIを実行し、メッセージレベルでVisibility Timeoutを上書きしている (SQSの設定画面で設定するのはあくまでデフォルトのVisibility Timeoutである )

参考: Larvelの実装

  public function release($delay = 0)
    {
        parent::release($delay);

        $this->sqs->changeMessageVisibility([
            'QueueUrl' => $this->queue,
            'ReceiptHandle' => $this->job['ReceiptHandle'],
            'VisibilityTimeout' => $delay,
        ]);
    }

https://github.com/laravel/framework/blob/11.x/src/Illuminate/Queue/Jobs/SqsJob.php#L50-L59

つまり、どれだけSQSの設定でデフォルトのVisibility Timeoutを長くしてもLaravel Workerにより自動的に上書きされてしまうというわけです。

ちなみにLaravel Worker側でこの時間を調整するには --backoff オプションを設定する必要があります。

参考:

SQSとLaravel Workerのsleep, backoffオプションの関係性

起きた事象: エクスポネンシャルバックオフを実装したが、初回のリトライ処理が想定より遅れて実行されていた。

Laravel Workerではリトライの遅延をかける際に --backoff オプションを使用することができますが、エクスポネンシャルバックオフを使う場合は自分で実装する必要があります。

↓ backoffメソッドがあればそちらを適用してくれる

    protected function calculateBackoff($job, WorkerOptions $options)
    {
        $backoff = explode(
            ',',
            method_exists($job, 'backoff') && ! is_null($job->backoff())
                        ? $job->backoff()
                        : $options->backoff
        );

        return (int) ($backoff[$job->attempts() - 1] ?? last($backoff));
    }

https://github.com/laravel/framework/blob/11.x/src/Illuminate/Queue/Worker.php#L606-L616

サーバーへの過負荷を避けるため徐々に遅延を大きくするエクスポネンシャルバックオフを実装しましたが、リトライ処理が想定より遅いタイミングで実行されていました。

こちらについても色々と調査したところ下記が原因であると判明しました。

原因: Worker実行時の --sleep オプションでbackoffメソッドが返す値より長い値を設定していた

sleepオプションはジョブがキューにない場合に、設定された時間だけプロセスをsleepするというものです(参考: Queues - Laravel 11.x - The PHP Framework For Web Artisans)。先ほどのLaravel Workerのリトライ実装の中で記述したVisibility Timeoutの件も考慮すると今回起きた事象に説明がつきます。

わかりやすいように仮にsleepオプションを1分、backoffメソッドで30秒を返したとすると下記の挙動になります。(簡略化のためWorkerプロセス数, キューに入っているジョブは1つとします)

  1. キューに入ったジョブをWorkerが取得しに行く(このタイミングでデフォルトのVisiblitiy Timeoutが設定される)
  2. 何らか理由でジョブの実行に失敗した場合、Workerによりbackoffに設定された30秒がこのジョブのVisibility Timeoutに設定される(デフォルトのVisibility Timeoutを上書きする)
  3. Workerが30秒経過する前にキューのメッセージを確認しにいく
  4. Visibility Timeoutによりキューが空に見えるのでsleepオプションで指定した通り、1分間スリープ状態になる
  5. Visibility Timeoutが過ぎ、キューに入っているジョブが見えるようになる
  6. Workerプロセスがスリープから復旧し、キューに入ったジョブを取りに行く

図にすると下記の通りです。Visibility Timeoutが過ぎていてもSleepの時間が長ければその分リトライ処理が待たされることになります。

まとめ

Laravel Worker x SQSの組み合わせで少しつまづいた点を2点ご紹介しました!Laravelのコンテナ化事例が少なく試行錯誤が大変でした。他にもジョブの失敗時の挙動をDLQで制御するためにカスタマイズ等を行なうなどしましたが、こちらは下記の記事が大変参考になりました。大変感謝です!!この記事がどなたかのお役に立っていますと幸いです。

tech.macloud.jp