Blogブログ

laravel

redis

horizon

2022.03.27

Laravelとhorizon-supervisorの解説(redis落ちたら終わり^^b)

laravelのキューにjobを入れる方法を記載していきます。

queueの作成とjob実行の流れ、

まずコンテナに入り、

php artisan make:job QueueTest

を実行しましょう。
そうすると、
App配下にJobsフォルダができて、その中にQueueTest.phpができています。

これから、あるページにアクセスしたり、あるボタンを押したりしたときに
このQueueTest.phpのhandle()に入ってくるようになります。

今はQueueTestという名前ですが、
実際はメール用のQueueならQueueEmail.phpや通知用のQueueNotice.phpとなり、そこにjobが入ってくるというイメージです。

これはまずおいといて、
例えばfollowなんかをされたりしたときに通知を送りたいなと思ったら、
まずこのqueueTestをインスタンス化します。
そのときの__construct()で、Idを渡したりして、準備しておきます。

例えばこんな感じになります。

<?php

namespace App\Jobs;

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

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

    private $userId;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($userId)
    {
        $this->userId = $userId;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        //今回はログに書くだけだけど、実際はメールを飛ばしたりDBに登録したりする
        Log::info('Queue Test has been executed. ID is ' . $this->userId);
    }
}

そしてコントローラー等で、実際に動かしたいところに↓このように書きます。

dispatch(new QueueTest($userId));
//本当はdispatch(new QueueTest($userId))->onQueue('queue_test');
//みたいに実行するけどそれは下記を見てください。

これで、キューにjobが登録されました。
キューとは例えるなら実行されるjobを登録して順番待ちさせるリストです

では、たまったjobを実行します。

今この瞬間はjobがキューというリストに登録されただけで、
実行されません。
実行のコマンドは

php artisan queue:work

です。
これを一度実行しておけばもうずっと実行状態になりますので、
このコマンドは実は初めの一回だけやれば大丈夫です。
つまり次からdispatch(new QueueTest($userId));とやったらいつでもjobが実行されます。

これでjobとqueue自体は終わりです。

queueの監視について

今例えば、あるコントローラーのアクションにこのdispatchが書かれていて、
同時に1000件のアクセスがあったので、1000件のjobがキューに登録されていて
もう以前にphp artisan queue:workも実行してあったので、
順番にこのキューに入ってるjobを実行していってる状態だとしましょう。

そうすると、

「今どこまでのjobを実行したのかな?」
「jobってそもそも実行されてるのかな?落ちたりしていないよな?」

ということが気になりますね。
それを監視するツールがあります。

horizonを使う事でjobの実行をダッシュボードで確認できる

Horizonのインストール

まずはインストール

composer require laravel/horizon
php artisan horizon:install

これで完了です。
もしバージョンを古くしないとcomposerが動かなかったら、
composer require laravel/horizon ^4.0
とかに変えてもらえればOKです。

ではhorizonにアクセスしてみます。

アクセスは/horizonとやればアクセスできます。
私の場合はポートが8081なので、
http://127.0.0.1:8080/horizon/
にアクセスすると…

のように見ることができるようになります。
実際に運用すると

こんな感じになります。
この説明をする前に、
app配下のconfigフォルダを見てください。
horizon.phpというファイルができています。
それを開くと、一番下に..

   'environments' => [
        'production' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['default'],
                'balance' => 'simple',
                'processes' => 10,
                'tries' => 1,
                'nice' => 0,
            ],
        ],

        'local' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['default'],
                'balance' => 'simple',
                'processes' => 3,
                'tries' => 1,
                'nice' => 0,
            ],
        ],
    ],

こう書いてありますよね。
ここが重要で、
実際にちょっと修正したファイルを見るとイメージが湧くと思います。

↓これを見てください。

            'supervisor-queue_test' => [
                'connection' => 'redis',
                'queue' => ['queue_test'],
                'balance' => 'simple',
                'processes' => 6,
                'tries' => 3,
                'timeout' => 360,
            ],

見てのとおりなのですが、
dashbordで表示するキューに名前をつけれます。queur_testとか。
そして、同時に実行できる数(プロセス)は6
失敗しても3回はリトライする、
タイムアウトは360秒。

このbalanceというのは、simpleとautoとfalseがあるんですが、
autoはプロセスのminとmaxが設定されてるときにキューの負荷状態を見て、どのプロセスにjobを割り振るかを自動でやってくれる設定です。
simpleはわりあてられたプロセス数だけを単純に実行します。

今、defaultからqueue_testにしたので、もう一回実行するとダッシュボードにqueue_testの項目が増えるはずです。

supervisor-1というのはただの文字列で、
ここの文字列をかえて配列を増やす事で
複数のqueueを管理できるようになります。

dispatchを実行するときにどのキュー(supervisor-1とか)に入れるかを設定します。

さっき上のほうで、
dispatch(new QueueTest($userId))->onQueue(‘queue_test’);
のように本当は実行すると書きましたが、
その意味もこれで、わかると思います。

実際のキューの監視と実行は…

先ほど上で、
jobの実行は、
php artisan queue:work
と書きましたが、
horizonが入ってるなら、
php artisan horizon
の実行で、horizonのconfigをみて必要なキューの名前の監視をしてくれて、
且つ、php artisan queue:workも実行してくれます。
(なので本来こちらを実行します。)

もしphp artisan horizonで Class ‘Predis\Client’ not found
というエラーが起きたら、
composer require predis/predis
を実行してからもう一度実行しましょう。

もし、
Predis\Connection\ConnectionException
というエラーが出た場合、
rediusのコンテナを忘れていますので、
docker-compose.ymlに
redis:
image: redis:latest
volumes:
– ./redis/data:/data
ports:
– 6379:6379
を追加してください

それでもダメだという人は、
たぶん.envのredisのHOSTがコンテナ名になっていないとかだと思います。

これでlaravelのjob queueとhorizonは完成です。

意外と簡単ですね。

ちなみにjobが失敗したりしたらhorizonではこのように見れます。

しかし、落ちたら終わりです^^b

どういうことかというと、
horizon自体が落ちたとき、
jobの実行できなくなりますよね。
redisにはjobは入ってるのですが、実行してくれなくなってしまいます。
そのために、horizon自体の監視と再起動をしてくれるのがsupervisorです。

先ほどの配列の文字列のsupervisor-1とこのsupervisorはまったく無関係なので注意です。

ちなみに、弊社は3000万人のデータベースのプロジェクトを現在進行形でやっていますが、数百万件とredisが入ったりするプロジェクトをやっていると…

redis自体が落ちたりする可能性もでてきます。

そうしたらjob自体が消えて本当に終わるので、
絶対にそうしないようにプロセスをわけたり、キューをわけたりして
駆使をして落ちないように対応をしていきます。

では、supervisorの設定を行なっていきます。

まずはミドルウェアの概念を理解しましょう。

一般的にあらゆるアクセス(ページがロードされたり)やpostするタイミング等々をroute/web.phpにroutingとして登録しておき、
そのweb.phpで設定したタイミングが来たときに
そのrequestオブジェクトが実行されるよりも早く実行されるのが
ミドルウェアです。

horizonが/horizonとアクセスしただけで全員に見られるのはかなりやばいですよね。
なので、まずはadminでログインしているときだけ、
/horizonが有効になるように設定します。

laravelではミドルウェアを作成するコマンドが用意されています。

例としてよくでてくるのがログインチェックなので、その話をします。

まずこれを実行します。

php artisan make:middleware AdminAuth

これを実行するとミドルウェアが生成されます。
このAdminAuthというのは自分で決めたファイル名です。
app/Http/Middleware配下に AdminAuth.phpが生成されているのがわかると思います。

このファイルを開いてみるとhandle()という関数がありますが、
リクエストよりも前にこの関数が実行されるので、
ここで処理を行い、requestオブジェクトに入れてしまおうというのが流れになります。

AdminAuth.phpの中身をこのようにします。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class AdminAuth
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $authed = Auth::guard('admin')->check();
        if (!$authed) {
            return redirect()->route('admin.login');
        }
        $user = Auth::guard('admin')->user();
        view()->composer('admin.*', function($view) use ($user) {
            $view->with('admin', $user);
        });
        return $next($request);
    }
}

つまり、今ログインチェックの機能を書きました。
ログインに関してはこのブログを見ておいてください。

このミドルウェアをミドルウェアとして実行することを記載するファイルがあります。

/Http配下にKernel.phpというファイルがあるのでそこに記載します。

middleWareGroupsとrouteMiddleWareの配列が目に入ると思います。web.phpのようなルーティング全体に対して実行させたい時はmiddleWareGroupsに書いていきます。

個別設定はrouteMiddleWareに書いていきます。今回はこっち。

配列の中に

'auth.admin' => \App\Http\Middleware\AdminAuth::class,

を追加してもらえれば大丈夫です。

ではroutingに追加します。
adminについてのログインのroutingだけなので前のブログでやったことと同じです。

Route::group(['prefix' => 'admin', 'namespace' => 'Admin', 'as' => 'admin.'], function($router) {
    $router->group(['middleware' => 'auth.admin'], function($router) {
        // AdminAuthを適用させたいroutingをこの中に定義する        
    });
});

↑重要なのはこれまでのadimin配下に適応させると書いていた部分に[‘middleware’ => ‘auth.admin’]を追加しただけです。

次に
app/config/horizon.php
を、

このように直します。

これでMiddleWareによるadmin配下のみだけのhorizon実行はおわりです。

安全になったところでsupervisorの設定をします。

もう一度おさらいですが、
そもそもsupervisiorを入れる理由はhorizonが落ちたときに
再起動をするためのものでした。
なのでその機能が他で実現できるならいらない設定です。

コンテナに入ってインストールしましょう。

apt-get install supervisorで
supervisorをインストールします。

$ apt-get update && apt-get install supervisor
$ cd /etc/supervisord
$ cd conf.d
$ touch horizon.conf 

このhorizonを編集します。

[program:horizon]
process_name=%(program_name)s
command=php /var/www/html/{$directly名} artisan horizon
autostart=true
autorestart=true
user=root
redirect_stderr=true
stdout_logfile=/var/log/supervisord.log

{$directly名}は自分のlaravelプロジェクト名にしてください

あとはサーバーで実行するだけです。

$ service supervisord start
$ service supervisord status
$ service supervisord stop

startだけでいいですが、これで監視とhorizonの自動リスタートが完了しました。

supervisorを入れなくてもコンテナの設定で実現する方がいい

supervisorは色々な設定があって詰まることも多いので、
docker-compose.ymlにsupervisorと同等の機能があるので、
そこで設定して終わらせた方がいいという感じもします。
同等の機能とは、horizonが落ちたらコンテナを破棄して、
新しいコンテナを作ればいいという機能があればいいことになります。

そこで、ymlをこう付け足してください。

  queue:
    build: ./php
    volumes:
      - ./{$プロジェクトまでのpath}:/var/www/html:delegated
      - ./php/php.ini:/usr/local/etc/php/conf.d/php.ini
    depends_on: ["mysql", "redis"]
    entrypoint: ["php", "artisan", "horizon"]
    working_dir: /var/www/html/{$directly名}

entrypoint: [“php”, “artisan”, “horizon”]
この1行だけで、このqueueのhorizonが落ちたら、
php artisan horizonを実行してくれるようになります。

この1行は、コンテナが立ち上がったタイミングで、
php artisan horizonを実行してくれると同時に
このコンテナのプロセスが落ちたらコンテナの再起動もしてくれます。
じゃあもともとentrypoint: [“php”, “artisan”, “horizon”]をプロジェクトのコンテナのymlに書けばいいじゃないかと思うかもしれませんが、そしたら、プロジェクトのコンテナを壊してまた再起動することになるのでそれはやめましょう。

これで、本当のプロジェクトに耐えられるqueueの設定は終わりです。

だいぶわかりやすく書けたのではないかと思います。
ではまた。