【CentOS 8／Apache 2.4】Laravel 5.5でメール送信をキューイングを用いて速くする【非同期化】

# 1. 概要
本記事では`CentOS`・`Apache`・`Laravel`環境下での`Mailファサード`によるメール送信において、**キューイング**を用いることで**非同期化**させ、**処理速度を向上させる**方法について紹介します。

まず、**キュー**とは「**先入れ先出しの規則でデータを取り扱うデータ構造**」を指します。

>キューとは、最も基本的なデータ構造の一つで、要素を入ってきた順に一列に並べ、先に入れた要素から順に取り出すという規則で出し入れを行うもの。順番を待つ人の行列と同じ仕組みであるため「待ち行列」とも訳される。

>引用元：IT用語辞典 e-Words「[キュー（待ち行列）とは](https://e-words.jp/w/%E3%82%AD%E3%83%A5%E3%83%BC.html)」

このキューを用いてデータの管理を行うことを**キューイング**といい、これにより処理を**非同期化**させることができ、同期的に処理する場合に比べて**処理速度が向上**します。
![キューイングのイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598695/045d20cd-bb70-fa2c-fc05-32c9ee7a70de.png)
上図の左側のように、同期処理では**一つの処理が完了するまで次の処理は実行されません**。この為、メール送信ような重い処理を挟んだ場合に**リソースのムダ**及び**不要な待ち時間**が発生してしまいます。

そこで、上図の右側のように、重い処理を一旦キューに送って**後続処理を優先**させ、**バックグラウンドでキューの処理を行う**ことで上記の問題を解決できます。

Laravelプロジェクトでのメール送信は`Mailファサード`を用いるのが一般的ですが、`Mailファサード`には同期的にメール送信する`sendメソッド`の他に、**キューイング**する`queueメソッド`がデフォルトで用意されています。

しかし、こちらのメソッドは**メール送信ジョブをエンキューするのみ**で、ジョブをバックグラウンドで永続的に処理させるには**プロセスモニタ**をOSに別途インストールする必要があります。

[公式マニュアル(Laravel 5.5)](https://laravel.com/docs/5.5/queues#supervisor-configuration)では、プロセスモニタとして`Supervisor`が紹介されていますが、載っているコマンドが`Ubuntu`へのインストールの場合のものであるため、本記事では**`CentOS`へのインストール方法**を紹介します。

# 2. メール送信を非同期化させる手順
## 2-1. 前提条件
- `CentOS 8`
- `Apache 2.4`
- `PHP 7.4`
- `Laravel 5.5`
- プロジェクトのルートディレクトリ名は`laravel`とします
- プロジェクトのルートディレクトリの絶対パスは`/var/www/html/laravel`とします

本記事では上記環境であるとの前提で解説します。

参考にされる場合は適宜読み替えて下さい🙇‍♂️

## 2-2. キュードライバーの設定
まず、キューイングを行う際の**ドライバーを指定**します。Laravel指定できるキュードライバーには次の6種類があります。

- `sync`
 - 同期処理、ローカル用途、デフォルトではこれが指定されている
- `database`
 - データベースにキュー用のテーブルを作成してそれを利用する
- `redis`
 - Redisを利用する
- `sqs`
 - Amazon SQSを利用する
- `beanstalkd`
 - Beanstalkdを利用する
- `null`
 - キューされたジョブを破棄する

本記事ではライブラリを別途インストールする必要がなく、既存のDBにテーブルを追加するだけで利用できる`database`を用います。

`.env`ファイルにてキュードライバーを指定します。

```configuration:.env
QUEUE_DRIVER=database
```
念のため[^1]`config/queue.php`でも、キュードライバーの指定を変更します。

[^1]: [`env`メソッド](https://readouble.com/laravel/5.5/ja/helpers.html#method-env)の第二引数はデフォルト値の指定で、第一引数がセットされていない／読み込めない場合に用いられます。指定しなくても通常は問題ありませんが、ここでは予想外の不具合に備えて変更しています。

```php:config/queue.php
    /*
    |--------------------------------------------------------------------------
    | Default Queue Driver
    |--------------------------------------------------------------------------
    |
    | Laravel's queue API supports an assortment of back-ends via a single
    | API, giving you convenient access to each back-end using the same
    | syntax for each one. Here you may set the default queue driver.
    |
    | Supported: "sync", "database", "beanstalkd", "sqs", "redis", "null"
    |
    */

    // envメソッドの第二引数をデフォルトの`sync`から指定したいドライバーに変更
    'default' => env('QUEUE_DRIVER', 'database'),
```

## 2-3. キューとなるテーブルの作成
次に、**キューとなるテーブルを生成するマイグレーションファイルを作成**します。

```console:ターミナル
php artisan queue:table
```
また、失敗した処理を保存するテーブルを作成するマイグレーションファイルを同時に作成しても良いです。こちらは必須ではありません。

```console:ターミナル
php artisan queue:failed-table
```
マイグレーションを実行しテーブルを作成します。

```console:ターミナル
php artisan migrate
```

## 2-4. メール送信でキューイングを使用する
**`Mailファサード`で`queueメソッド`を用いる**だけです。これだけでメール送信のジョブが上述のマイグレーションで作成された`jobs`テーブルに投入されます。

```php:mailableクラス
Mail::to($request->user())
    ->queue(new OrderShipped($order));
```
### ❗️`queueメソッド`の注意点❗️
`queueメソッド`を用いる場合、`Mailableインスタンス`（ここでは`OrderShippedクラス`のインスタンス）の引数として**PHPで定義済みのクラスを含むオブジェクト型の多くは渡すことができない**[^2]点に注意が必要です。

[^2]: 一部渡すことのできる例外はあるようです。しかし、ほとんどのオブジェクト型でエラーが出ると考えられますので、オブジェクト型以外の型を渡しておく方が無難です。

例えば、ユーザーがフォームに入力した情報をメール内で使用するために、`Requestクラス`のインスタンスをオブジェクト型として格納した`$request`を引数に渡す場合を考えます。

この場合、`sendメソッド`では問題なく送信できますが、`queueメソッド`では例外`Serialization of 'Closure' is not allowed`が発生します。

```php:mailableクラス
// 問題なし
Mail::to($request->user())
    ->send(new OrderShipped($request, $order));

// エラー発生
Mail::to($request->user())
    ->queue(new OrderShipped($request, $order));
```
この問題は、引数を**オブジェクト型以外の型として渡す**ことで回避できます。
メールテンプレートでどの様にユーザーのリクエストボディにアクセスするかによりますが、具体的には次の様な方策が考えられます。

```php:mailableクラス
// 数値型や文字型のプロパティとして渡す
Mail::to($request->user())
    ->queue(new OrderShipped($request->name, $order));

// 配列として渡す
Mail::to($request->user())
    ->queue(new OrderShipped($request->input(), $order));
```
配列として渡す場合、`inputメソッド`の他

- `allメソッド`
- `exceptメソッド`
- `onlyメソッド`

も使用することができます。詳しくは下記記事が参考になります。
Qiita「[Requestの各メソッド（query(), get(), all()...）の使い分け](https://qiita.com/piotzkhider/items/feaba3acda27d2e432d8)」 by @piotzkhider さん

また、メールテンプレートで`$request->name`のようにアロー演算子を用いてプロパティにアクセスしている場合、こちらも`$request['name']`のように配列の値を参照するように変更しましょう。

#### ❓オブジェクト型が渡せない理由❓
キューイングの際の**シリアル化ができない場合がある**ためです。

先述したように、`Requestインスタンス`を渡して`queueメソッド`を使用すると、例外`Serialization of 'Closure' is not allowed`が発生します。直訳すると「『クロージャ』のシリアル化は許可されていません」となります。

**シリアル化**とは「**値の型や構造を保った状態でデータの受け渡しや保存ができる状態にすること**」を意味し、今回の場合は`jobs`テーブルに保存する際に、型や構造の情報が失われないように値のシリアル化が行われているはずです。

参考：侍エンジニア塾ブログ「[PHPのシリアライズを知ろう！便利な使い方徹底解説](https://www.sejuku.net/blog/80079)」

シリアル化にはPHPの変数操作関数である`serialize`が用いられていると推測され、マニュアルには次の様に記載されています。

> serialize() は、resource および一部の object 以外のすべての型を処理します。
>（中略）
>注意:
>PHPの組み込みオブジェクトの多くはシリアル化できないことに注意しましょう。

>引用元：PHPマニュアル「[PHP: serialize - Manual](https://www.php.net/manual/ja/function.serialize.php)」

組み込みオブジェクトとは、PHPに標準で組み込まれているオブジェクトであり、その中に`Closure`クラスがあります。

参考：PHPマニュアル「[PHP: 定義済のクラス - Manual](https://www.php.net/manual/ja/reserved.classes.php)」

つまり、エラーメッセージ「`Serialization of 'Closure' is not allowed`」が示している通り、定義済みのクラスである`Closure`が`Requestオブジェクト`に含まれているためにシリアル化が出来ずに例外が発生したと考えられます。

## 2-5. `Supervisor`のインストール
`CentOS 8`へのインストール方法としては次のWebページが参考になります。

参考：CloudWater「[Installing Supervisor on CentOS 8](https://cloudwafer.com/blog/installing-supervisor-on-centos-8/)」

インストールする前にシステムパッケージを最新の状態にアップデート。

```console:ターミナル
sudo dnf update -y
```
次いで、`CentOS`標準のリポジトリでは提供されていないパッケージを、`yumコマンド`でインストール可能にする`EPELリポジトリ`をインストールし、アップデート。

参考：Webセキュリティの小部屋「[CentOS に EPEL リポジトリを追加する](https://www.websec-room.com/2014/01/15/1535)」

```console:ターミナル
sudo dnf install epel-release
sudo yum update
```
`Supervisor`をインストールする。

```console:ターミナル
sudo yum -y install supervisor
```

## 2-6. ログファイルの作成
キューワーカーをバッググラウンドで実行させた場合の**ログを記録するファイルを追加**します。

ログファイルの配置場所は自由に決められますが、今回はプロジェクトのルートディレクトリ内の`laravel/storage/logs`内に配置し、ファイル名は`supervisor.log`とします。

下記コマンド実行後に`:wq`で空ファイルとして保存します。

```console:ターミナル
vi /var/www/html/laravel/storage/logs/supervisor.log
```

本記事では、バックグラウンドプログラムは`apache`が実行するので、ファイルの**所有者とパーミッションを変更**します。パーミッションは所有者以外が書き込みできないように`744`とします。

```console:ターミナル
sudo chown apache:apache /var/www/html/laravel/storage/logs/supervisor.log
sudo chmod 744 /var/www/html/laravel/storage/logs/supervisor.log
```
参考：「[パーミッション早見表](http://hayakawa.ddo.jp/beginner/ux/permit.html)」

## 2-7. 設定ファイルの編集
`Supervisor`のインストールにより、設定ファイル`/etc/supervisord.conf`が追加されているはずです。このファイルで**追加の設定ファイルを読み込む記述を追記**します。

```console:ターミナル
sudo vi /etc/supervisord.conf
```

```conf:/etc/supervisord.conf

# （中略）

[include]
files = supervisord.d/*.ini
# 下記行を追加 
files = supervisord.d/*.conf
```
さらに、`/etc/supervisord.conf`と同時に`/etc/supervisord.d`ディレクトリも追加されているはずなので、そのディレクトリ内に**読み込ませる設定ファイル`/etc/supervisord.d/laravel-worker.conf`を作成**します。

```console:ターミナル
sudo vi /etc/supervisord.d/laravel-worker.conf
```

```conf:/etc/supervisord.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10
autostart=true
autorestart=true
redirect_stderr=true
numprocs=8
user=apache
stdout_logfile=/var/www/html/laravel/storage/logs/supervisor.log
```

キューワーカーの実行コマンドは`php artisan queue:work`ですが、`apache`が実行できるように**絶対パスで記述**します。

今回はオプションとして最大試行回数とスリープ時間を`--sleep=3`、`--tries=10`として指定しましたが、他にも様々なオプションがあります。

参考：ReadDouble「[Laravel 5.5 キュー](https://readouble.com/laravel/5.5/ja/queues.html)」

また、`stdout_logfile`には先程作成した**ログファイルの絶対パスを指定**します。

コピペする際には、`command`や`stdout_logfile`のパスをそれぞれご自身のプロジェクトでのパスに置き換えて下さい。

他の項目についての詳細については下記をご参照下さい。

参考：Supervisor 4.2.1 documentation「[Configuration File](http://supervisord.org/configuration.html#program-x-section-settings)」

## 2-8. デーモン化と起動
設定が完了したので、**アプリ起動時にプロセスが自動実行**されるようにします。

```console:ターミナル
sudo systemctl start supervisord
sudo systemctl enable supervisord
```
動作確認などで既にスタートしていた場合や、上記の`conf`ファイルを編集した場合は再起動させましょう。

```console:ターミナル
sudo systemctl restart supervisord
```
プロセスがちゃんと動いているかどうか確認したい場合は以下のコマンドを実行します。

```console:ターミナル
sudo systemctl status supervisord
```
下記のように`Active: active (running)…`と表示され、`/etc/supervisord.d/laravel-worker.conf`の`command`項目で設定したコマンドが`numprocs`項目で設定した回数分表示されていれば問題ありません（下記では`301829`〜`301836`のプロセス）。

```console:ターミナル
● supervisord.service - Process Monitoring and Control Daemon
   Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2021-01-10 xx:xx:xx JST; xs ago
  Process: 301824 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code=exited, status=0/SUCCESS)
 Main PID: 301828 (supervisord)
    Tasks: 9 (limit: 5049)
   Memory: 144.6M
   CGroup: /system.slice/supervisord.service
           ├─301828 /usr/bin/python3.6 /usr/bin/supervisord -c /etc/supervisord.conf
           ├─301829 php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10
           ├─301830 php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10
           ├─301831 php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10
           ├─301832 php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10
           ├─301833 php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10
           ├─301834 php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10
           ├─301835 php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10
           └─301836 php /var/www/html/laravel/artisan queue:work --sleep=3 --tries=10

 1月 10 xx:xx:xx 160-251-23-222 systemd[1]: Starting Process Monitoring and Control Daemon...
 1月 10 xx:xx:xx 160-251-23-222 systemd[1]: Started Process Monitoring and Control Daemon.
```

これでキューワーカーをバックグラウンドで実行させることができました🎉

不備や改善点がございましたら、コメント下さるとありがたいです🙇‍♂️

# 3. 参考記事一覧

- IT用語辞典 e-Words「[キュー（待ち行列）とは](https://e-words.jp/w/%E3%82%AD%E3%83%A5%E3%83%BC.html)」
- Laravel Documentation「[Queues](https://laravel.com/docs/5.5/queues#supervisor-configuration)」
- ReadDouble「[キュー 5.5 Laravel](https://readouble.com/laravel/5.5/ja/queues.html)」
- ReadDouble「[メール 5.5 Laravel](https://readouble.com/laravel/5.5/ja/mail.html)」
- Qiita「[LaravelでMailableなオブジェクトをqueueで送信したら"Serialization of 'Closure' is not allowed"というエラーになった件の解決方法](https://qiita.com/pinekta/items/f95742547f9fd8b2a446)」 by @pinekta さん
- Qiita「[Requestの各メソッド（query(), get(), all()...）の使い分け](https://qiita.com/piotzkhider/items/feaba3acda27d2e432d8)」 by @piotzkhider さん
- 侍エンジニア塾ブログ「[PHPのシリアライズを知ろう！便利な使い方徹底解説](https://www.sejuku.net/blog/80079)」
- PHPマニュアル「[PHP: serialize - Manual](https://www.php.net/manual/ja/function.serialize.php)」
- PHPマニュアル「[PHP: 定義済のクラス - Manual](https://www.php.net/manual/ja/reserved.classes.php)」
- CloudWater「[Installing Supervisor on CentOS 8](https://cloudwafer.com/blog/installing-supervisor-on-centos-8/)」
- Webセキュリティの小部屋「[CentOS に EPEL リポジトリを追加する](https://www.websec-room.com/2014/01/15/1535)」
- 「[パーミッション早見表](http://hayakawa.ddo.jp/beginner/ux/permit.html)」
- Supervisor 4.2.1 documentation「[Configuration File](http://supervisord.org/configuration.html#program-x-section-settings)」
- 366Service「[Laravelキューを永久に実行するためにcentos 7にsupervisordをインストールして設定する](https://www.366service.com/jp/qa/e431fb8eab6bd99dfcb8ec523b1a29cc)」
- 思考の葉「[Laravel Queueを利用して非同期によるメール送信](https://noumenon-th.net/programming/2020/06/02/laravel-queue/)」