【Laravel】Queue・Job・Workerを理解する!

Laravel
この記事は約7分で読めます。

はじめに

Laravel に限らず、テーブル( Queue ) に処理( job ) を入れて実行するというのはどういったメリットがあるのでしょうか?

わい
わい

一番のメリットは、重い処理を非同期で実行してくれる点だと思います。

クロ
クロ

なんのこっちゃ~~~

極端の例で言うと、
例えば、ユーザーが登録したデータを他のサーバーにあるテーブルにも格納するといったシーンがあるとしましょう。1
そのとき、他のサーバーにまずApiを叩く処理が必要なことと、他のサーバーの登録処理より前にバリデーションなどの他の処理が多かったとき、レスポンスが返ってくるまで操作したユーザーは待たなくてはいけません。

そうすると、操作しているユーザーはどう思うでしょうか?

クロ
クロ

おっそいにゃ~~~~。

クロたんみたいに、レスポンスが遅くて再度操作を行うか、リロードされたり、閉じられたりする可能性が非常に高くなります。( ユーザービリティーが悪い! )

なので、非同期で行える重いと予見される処理は、テーブル( Queue ) に処理( job )入れて、他のプロセス ( Worker )に処理を実行してもらおうというのが、今回のテーマです!
それでは、Laravel のプロジェクト作成から説明していきたいと思います!

まずは処理の流れをつかむ!

  • Laravel8 プロジェクト作成 ( version指定してます )
composer create-project laravel/laravel=8.* project_name
  • 環境変数を編集
# QUEUE_CONNECTION=sync
QUEUE_CONNECTION=database

デフォルトでは、sync(同期) が指定されています。
sync と database の違いについては、脚注2をご覧ください。

  • jobsテーブル( Queue ) のmigrationを作成
php artisan queue:table
クロ
クロ

テーブル名が jobsテーブル だからちょっと紛らわしいにゃ~。

ケン
ケン

確かに。。。テーブルは箱のイメージだから Queue とマッチするからいいけど、
格納されるレコードは処理内容 ( job )だから、筋は通っているかな。

  • job 作成
php artisan make:job TestJob
  • 以下を記述
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;

class TestJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        /** ↓だけ追記。この job は/storage/app 直下に job.txt を作成する */
        \Storage::put('job.txt', date('Y-m-d H:i:s') . PHP_EOL, FILE_APPEND);
    }
}
  • 検証

コンソールを二つ用意してください。

1つには↓を

php artisan /path/to/project/artisan queue:work

/path/to/project には、artisanファイル が格納されているディレクトリパスを指定する。

もう1つには↓を

php artisan /path/to/project/artisan tinker
Psy Shell v0.11.20 (PHP 7.4.33 — cli) by Justin Hileman
> \App\Jobs\TestJob::dispatch();
↑作成した TestJob に dispatch() して処理を委譲。
ここではつまり、jobsテーブル ( Queue ) に 処理 ( job ) を格納している。
ケン
ケン

artisan tinker コマンドは非常に便利です。
上記の > 以降の記述のように、コンソール上で処理をかけるので、動作確認・テストしたいときとかは非常に重宝します!

  • queue:work を実行したコンソールに job処理完了メッセージ が表示されます。
[2023-09-13 05:42:25][19] Processing: App\Jobs\TestJob
[2023-09-13 05:42:28][19] Processed:  App\Jobs\TestJob

上記のような流れで Queue・Job・Worker は処理をします。
なので、この処理は別にリアルタイムでなくていいし重いから、ユーザーへのレスポンスを考えて Queue にぶち込んで Worker に処理をしてもらおうといった思考回路で実装していきます!

queue:listen と queue:work の違い

公式のドキュメントにもありますが、worker の実行 (つまり、Queue から job を取り出して実行する)には2通りのやり方があることに気づくかと思います。どちらを選ぶかは一長一短なので要件次第です。

ざっくり違いを言うと、デーモンプロセス( queue:work ) かそうでないか (queue:listen ) の違いになります。

デーモンプロセスでわかりやすいのが、webサーバーの apache 。
HTTP(S)リクエストがユーザーからあったら、ドキュメントルート直下のファイルをレスポンスする。
上記の動きからわかるように、apache はデーモンプロセスで、常にHTTP(S)リクエストを待ち構えている状態です。また、設定ファイルなどを編集したときは 再起動が必要 になります。
queue:work もそれと同じです。

一方、queue:listen は apache の例でいうと、HTTP(S)リクエストがユーザーからあったら apache がプロセスを起動してドキュメントルート直下のファイルをレスポンスする。
↑この動きは本来リクエストが一斉に集まったときにしかしませんが、レスポンスが遅くなる気がしませんか?事実遅くなります。ジャニーズのチケットweb販売とかで、めちゃくちゃレスポンス遅いけどなんとか開けたみたいなのってありますよね?プロセスを起動するのって結構パワーいるんですよね。。。
しかし、ここで queue:listen の場合は、実行ファイルが編集されても再起動が不要な点です。

クロ
クロ

このご時世にジャニーズはやめとけにゃ~。。。

なので、
開発中は変更内容が再起動していないが故に反映されていなかったりするので queue:listen で行い、
実運用では queue:work でやったりするのがいいかなと思います!
しかし、queue:work にもあまり意識することは少ない落とし穴があるので、脚注3 に書いておきます。

最後に

Laravelは多機能で便利な分、できることが多すぎてたまにイヤになるときがあり、ピュアな php で書いた方が楽かもなと感じるときがたまにあります。

しかし、ピュアな php で書いた方が楽 なんてことは100%ないですね!笑
ただ単に、Laravel 固有の書き方を新しく覚えるのが面倒だなと感じているだけです。
ピュアなphp で書こうと思ったら、コマンドデザインパターンなどをつかって実装することになるので非常に大変です。。。汗

わい
わい

今後も固有の書き方に臆せず、しっかりと学んでいきたいと思います。

次回は、 supervisor を使って、worker を監視する というのを紹介していきたいと思います!

クロ
クロ

楽しみだにゃ~~~。

  1. 非同期で処理を実行するので、ユーザー操作との時間差は、Queue の混み具合に依存します。なので、ユーザーへのレスポンスを早めるために何が何でも Queue で処理させようという発想は危険です。 ↩︎
  2. syncだと、テーブル ( Queue ) には挿入せず、すぐに(同期的)に処理を実行してしまうため、ソースコード上は job を使ってるけれども、それは処理を委譲しているだけで実質的には Jobs を使わない処理と変わらない。(つまり、どいつに処理をやらせるかを分けているだけにすぎない。)また、sync だと失敗したときのリトライ処理を自前で実装する必要がある↩︎
  3. デーモンプロセスはメモリに常駐して処理を行っているので、処理する内容 ( job ) が、例えば大きな画像ファイルを扱っていたりすると、その処理中でメモリリークを起こさないようにメモリを解放してあげる必要があります。 ↩︎

コメント