1. 概要
本記事ではCentOS
・Apache
・Laravel
環境下でのMailファサード
によるメール送信において、キューイングを用いることで非同期化させ、処理速度を向上させる方法について紹介します。
まず、キューとは「先入れ先出しの規則でデータを取り扱うデータ構造」を指します。
キューとは、最も基本的なデータ構造の一つで、要素を入ってきた順に一列に並べ、先に入れた要素から順に取り出すという規則で出し入れを行うもの。順番を待つ人の行列と同じ仕組みであるため「待ち行列」とも訳される。
引用元:IT用語辞典 e-Words「キュー(待ち行列)とは」
このキューを用いてデータの管理を行うことをキューイングといい、これにより処理を非同期化させることができ、同期的に処理する場合に比べて処理速度が向上します。
上図の左側のように、同期処理では一つの処理が完了するまで次の処理は実行されません。この為、メール送信ような重い処理を挟んだ場合にリソースのムダ及び不要な待ち時間が発生してしまいます。
そこで、上図の右側のように、重い処理を一旦キューに送って後続処理を優先させ、バックグラウンドでキューの処理を行うことで上記の問題を解決できます。
Laravelプロジェクトでのメール送信はMailファサード
を用いるのが一般的ですが、Mailファサード
には同期的にメール送信するsendメソッド
の他に、キューイングするqueueメソッド
がデフォルトで用意されています。
しかし、こちらのメソッドはメール送信ジョブをエンキューするのみで、ジョブをバックグラウンドで永続的に処理させるにはプロセスモニタをOSに別途インストールする必要があります。
公式マニュアル(Laravel 5.5)では、プロセスモニタとして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
ファイルにてキュードライバーを指定します。
QUEUE_DRIVER=database
念のため1config/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. キューとなるテーブルの作成
次に、キューとなるテーブルを生成するマイグレーションファイルを作成します。
php artisan queue:table
また、失敗した処理を保存するテーブルを作成するマイグレーションファイルを同時に作成しても良いです。こちらは必須ではありません。
php artisan queue:failed-table
マイグレーションを実行しテーブルを作成します。
php artisan migrate
2-4. メール送信でキューイングを使用する
Mailファサード
でqueueメソッド
を用いるだけです。これだけでメール送信のジョブが上述のマイグレーションで作成されたjobs
テーブルに投入されます。
Mail::to($request->user())
->queue(new OrderShipped($order));
❗️queueメソッド
の注意点❗️
queueメソッド
を用いる場合、Mailableインスタンス
(ここではOrderShippedクラス
のインスタンス)の引数としてPHPで定義済みのクラスを含むオブジェクト型の多くは渡すことができない2点に注意が必要です。
例えば、ユーザーがフォームに入力した情報をメール内で使用するために、Requestクラス
のインスタンスをオブジェクト型として格納した$request
を引数に渡す場合を考えます。
この場合、sendメソッド
では問題なく送信できますが、queueメソッド
では例外Serialization of 'Closure' is not allowed
が発生します。
// 問題なし
Mail::to($request->user())
->send(new OrderShipped($request, $order));
// エラー発生
Mail::to($request->user())
->queue(new OrderShipped($request, $order));
この問題は、引数をオブジェクト型以外の型として渡すことで回避できます。
メールテンプレートでどの様にユーザーのリクエストボディにアクセスするかによりますが、具体的には次の様な方策が考えられます。
// 数値型や文字型のプロパティとして渡す
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()...)の使い分け」 by @piotzkhider さん
また、メールテンプレートで$request->name
のようにアロー演算子を用いてプロパティにアクセスしている場合、こちらも$request['name']
のように配列の値を参照するように変更しましょう。
❓オブジェクト型が渡せない理由❓
キューイングの際のシリアル化ができない場合があるためです。
先述したように、Requestインスタンス
を渡してqueueメソッド
を使用すると、例外Serialization of 'Closure' is not allowed
が発生します。直訳すると「『クロージャ』のシリアル化は許可されていません」となります。
シリアル化とは「値の型や構造を保った状態でデータの受け渡しや保存ができる状態にすること」を意味し、今回の場合はjobs
テーブルに保存する際に、型や構造の情報が失われないように値のシリアル化が行われているはずです。
参考:侍エンジニア塾ブログ「PHPのシリアライズを知ろう!便利な使い方徹底解説」
シリアル化にはPHPの変数操作関数であるserialize
が用いられていると推測され、マニュアルには次の様に記載されています。
serialize() は、resource および一部の object 以外のすべての型を処理します。
(中略)
注意:
PHPの組み込みオブジェクトの多くはシリアル化できないことに注意しましょう。
引用元:PHPマニュアル「PHP: serialize - Manual」
組み込みオブジェクトとは、PHPに標準で組み込まれているオブジェクトであり、その中にClosure
クラスがあります。
参考:PHPマニュアル「PHP: 定義済のクラス - Manual」
つまり、エラーメッセージ「Serialization of 'Closure' is not allowed
」が示している通り、定義済みのクラスであるClosure
がRequestオブジェクト
に含まれているためにシリアル化が出来ずに例外が発生したと考えられます。
2-5. Supervisor
のインストール
CentOS 8
へのインストール方法としては次のWebページが参考になります。
参考:CloudWater「Installing Supervisor on CentOS 8」
インストールする前にシステムパッケージを最新の状態にアップデート。
sudo dnf update -y
次いで、CentOS
標準のリポジトリでは提供されていないパッケージを、yumコマンド
でインストール可能にするEPELリポジトリ
をインストールし、アップデート。
参考:Webセキュリティの小部屋「CentOS に EPEL リポジトリを追加する」
sudo dnf install epel-release
sudo yum update
Supervisor
をインストールする。
sudo yum -y install supervisor
2-6. ログファイルの作成
キューワーカーをバッググラウンドで実行させた場合のログを記録するファイルを追加します。
ログファイルの配置場所は自由に決められますが、今回はプロジェクトのルートディレクトリ内のlaravel/storage/logs
内に配置し、ファイル名はsupervisor.log
とします。
下記コマンド実行後に:wq
で空ファイルとして保存します。
vi /var/www/html/laravel/storage/logs/supervisor.log
本記事では、バックグラウンドプログラムはapache
が実行するので、ファイルの所有者とパーミッションを変更します。パーミッションは所有者以外が書き込みできないように744
とします。
sudo chown apache:apache /var/www/html/laravel/storage/logs/supervisor.log
sudo chmod 744 /var/www/html/laravel/storage/logs/supervisor.log
参考:「パーミッション早見表」
2-7. 設定ファイルの編集
Supervisor
のインストールにより、設定ファイル/etc/supervisord.conf
が追加されているはずです。このファイルで追加の設定ファイルを読み込む記述を追記します。
sudo vi /etc/supervisord.conf
# (中略)
[include]
files = supervisord.d/*.ini
# 下記行を追加
files = supervisord.d/*.conf
さらに、/etc/supervisord.conf
と同時に/etc/supervisord.d
ディレクトリも追加されているはずなので、そのディレクトリ内に読み込ませる設定ファイル/etc/supervisord.d/laravel-worker.conf
を作成します。
sudo vi /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 キュー」
また、stdout_logfile
には先程作成したログファイルの絶対パスを指定します。
コピペする際には、command
やstdout_logfile
のパスをそれぞれご自身のプロジェクトでのパスに置き換えて下さい。
他の項目についての詳細については下記をご参照下さい。
参考:Supervisor 4.2.1 documentation「Configuration File」
2-8. デーモン化と起動
設定が完了したので、アプリ起動時にプロセスが自動実行されるようにします。
sudo systemctl start supervisord
sudo systemctl enable supervisord
動作確認などで既にスタートしていた場合や、上記のconf
ファイルを編集した場合は再起動させましょう。
sudo systemctl restart supervisord
プロセスがちゃんと動いているかどうか確認したい場合は以下のコマンドを実行します。
sudo systemctl status supervisord
下記のようにActive: active (running)…
と表示され、/etc/supervisord.d/laravel-worker.conf
のcommand
項目で設定したコマンドがnumprocs
項目で設定した回数分表示されていれば問題ありません(下記では301829
〜301836
のプロセス)。
● 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「キュー(待ち行列)とは」
- Laravel Documentation「Queues」
- ReadDouble「キュー 5.5 Laravel」
- ReadDouble「メール 5.5 Laravel」
- Qiita「LaravelでMailableなオブジェクトをqueueで送信したら"Serialization of 'Closure' is not allowed"というエラーになった件の解決方法」 by @pinekta さん
- Qiita「Requestの各メソッド(query(), get(), all()...)の使い分け」 by @piotzkhider さん
- 侍エンジニア塾ブログ「PHPのシリアライズを知ろう!便利な使い方徹底解説」
- PHPマニュアル「PHP: serialize - Manual」
- PHPマニュアル「PHP: 定義済のクラス - Manual」
- CloudWater「Installing Supervisor on CentOS 8」
- Webセキュリティの小部屋「CentOS に EPEL リポジトリを追加する」
- 「パーミッション早見表」
- Supervisor 4.2.1 documentation「Configuration File」
- 366Service「Laravelキューを永久に実行するためにcentos 7にsupervisordをインストールして設定する」
- 思考の葉「Laravel Queueを利用して非同期によるメール送信」