タクトピクセル開発者ブログ

印刷・コンテンツ業界向けSaaS製品を開発・運営するタクトピクセル(株)の開発者ブログ

Laravelの非同期処理の使いどころ

Laravel (ララベル) はPHP言語向けのオープンソースWebフレームワークです。タクトピクセルでは、SPA構成のWebアプリケーションを開発する場合にWeb API実装のためのフレームワークとしてLaravelを良く使っています。

Laravelにはイベント・リスナーキュー・ワーカーという仕組みがあり、これらを利用することで非同期処理を手軽に実装することが出来ます。「非同期処理」といっても、その実装方法は目的に応じて選択する必要があり、実装の工数も変わります。経験を踏まえて、どのように非同期処理を実装したら良いかを考えてみました。

タクトピクセルでは、AWSのECSというコンテナ環境を利用しているため、その場合を前提として記述していますが、Kubernetesやほかのマネージドコンテナ環境でも基本的には同様の考え方になると思います。

非同期処理を使うためのインフラ構築

実装上はLaravelで用意されている仕組みを使えばいいのですが、これを実際に手元で動かしたり、クラウド環境で動作させる場合はインフラ部分を工夫する必要があります。

Laravel queue worker (キューワーカー)を使用して、非同期処理を行います。

readouble.com

workerはsupervisordなどを使って、別のプロセスで常時起動しておきます。LaravelのWeb APIのController等でジョブをキューに登録すると、workerはその情報を取得して処理を行います。処理の内容はjob(ジョブ)という単位で登録されます。

Web APIはワーカーの動きを特に把握せずにレスポンスを返すので、ジョブの完了を知るためには、DBのフィールドに処理ステータスを示すフラグを用意しておき、別途、非同期処理のステータス状況を取得するためのWeb APIを用意しておく必要があります。

キューのドライバーは複数から選択できます。簡単に実装するのであれば、MySQLなどのRDBで良いのですが、アクセスが多少多くなってくるとすぐにDBのデッドロックに陥ります。それを回避するためには、理想的にはキュー用のドライバーにはredisやmemcachedのようなDBを選択するのが安心です。

非同期処理を使わない場合

まず、非同期処理を使わない場合についても考えたいと思います。

リスクがありますが、テスト的なWeb API実装やクローズドな環境でのアプリケーションだと、非同期処理を実装する工数が確保できないこともあると思います。

Webフレームワークでは、何か情報をリクエストしたら、処理を行い、その結果を返すだけ、という処理が基本になりますが、時間のかかる処理の場合は非同期処理をする必要があります。基本的に、1つのリクエストに対してレスポンスが返ってくるまでには1秒未満が理想的で、最大でも30秒に設定されることが多いと思います。

この最大の時間はPHPの設定で変更することが出来ます。

php.iniで設定する場合、

max_execution_time = 300

のようにすることで、300秒(5分)に伸ばすことが出来ます。この変更は、Laravelのアプリケーション全体に適用されるために、利用には注意が必要です。この他、コード上でset_time_limitを設定することでも実現できます。

この場合、考えられる問題としては、処理時間が予め予測できないような処理を含んでいる場合に、最大時間以内に処理が終わらずにタイムアウトしてしまうということです。タイムアウトした場合は、処理は途中で中断されます。

これに限らないことですが、処理が途中で中断してもデータの不整合が起こらないように、データベース操作は適切なトランザクションを設定しておく必要があります。

処理が完了せずにタイムアウトを頻発させてしまう場合、ユーザーには非常に不安定なシステムとしてみなされ、システムのUXは著しく低下してしまいます。セキュリティについての懸念も抱かせてしまうでしょう。

非同期処理方法の選択方法

ちょっとした非同期処理(10秒~1分程度):イベント・リスナー

Laravelを使ってWeb APIを開発していくと、少し複雑な処理を実装しようとすると、非同期処理はすぐに必要になります。ローカルアプリケーションの場合と異なり、Web APIの場合は、処理時間の上限を気にする必要があるのですが、画像処理やほかのサービスに対して処理を投げるなど、処理時間がデータによって変化するようなリクエストを行う場合には非同期に処理を行う必要があります。

およそ10秒以上かかるようなリクエストの場合は、この非同期処理を検討するべきだと思います。この非同期処理の場合は、特に処理の結果や進捗状況を監視する必要が無いものを想定しています。例えば、以下の様なケースが考えられます。

  • メール配信
  • チャットへの通知処理
  • 外部サービスのWeb APIのリクエスト
  • 特別なログ出力(自社製アクセス解析等)

この場合は、イベント・リスナーが適しています。

readouble.com

イベント・リスナーは、処理の発行と購読が簡単に実装できる、オブザーバーパターンを踏襲した仕組みです。(別途、Eloquent modelのobserverというのもありますが、別の目的で使われるものなのでここでは割愛。)

例えば、ファイルを削除した場合に、「削除イベント」を発行しておきます。別途、ShouldQueueクラスを継承した「削除メール通知」のためのリスナーを登録しておけば、非同期に通知処理が行われます。追加の要望で、チャットツールへの通知処理を追加したい場合は、同様にリスナーを登録するだけで、処理が追加されます。

このように処理の発行と購読を抽象化することで、手軽に短い非同期処理を実装することが出来ます。

時間のかかるバッチ処理(10分程度):キュー・ワーカー・ジョブ

例えば、以下の様なケースではLaravelのQueue(キュー)の仕組みが適しています。

  • PDF形式の帳票出力を行いたい
  • ダウンロード用のCSVファイルを作成したい
  • ダウンロード用の圧縮ファイルを作成したい
  • 外部サービスに複数のリクエストを行って集計作業を行いたい
  • ちょっとした画像処理を行いたい

readouble.com

タクトピクセルでは、プルーフロッグ(デザイン・書類のためのオンライン校正検版ツール)の画像比較処理などに、この仕組みを利用しています。

この場合は、時間のかかる処理なので、途中経過を確認するための仕組みや、失敗したときのリトライの方法について仕組みを考える必要があります。単に1つのリクエストに紐づいてジョブを処理するだけではなく、そのほか以下の様なAPIを用意しておく必要があります。REST APIの範疇を超えてくるので、実装したいジョブの内容に応じて適切なAPIを設計する必要があります。

  • ジョブの実行
  • ジョブのステータス取得
  • ジョブの中断
  • ジョブのリトライ
  • ジョブ結果の取得
  • ジョブの削除

さらに時間のかかるバッチ処理(1時間以上):AWS Batch, AWS ECS sheduled tasks

この場合はLaravelのworkerとは別の仕組みで処理を行います。例えば、以下の様なケースを考えます。

  • 1GB以上のデータに対して集計処理を行いたい
  • 時間のかかる機械学習の学習演算処理を行いたい

この場合は考えることが多くなります。途中経過を確認するための仕組みや、失敗したときのリトライの方法について仕組みを考える必要があります。

タクトピクセルでは、プードル(印刷工場のための深層学習モデル作成ツール)にて、AWS Batchを使ったGPUインスタンスによる深層学習の画像処理で、この仕組みを利用しています。数日間から1週間以上のバッチ処理を行うこともできます。

AWSでインフラを構築している場合は、AWS ECSを使っている場合にはAWS Batchやscheduled tasksが利用できます。ジョブを処理するトリガーやキューシステムはLaravelの仕組みを離れるため、PHP/Laravel以外のPythonやJavaScript/Node.jsで実装したアプリケーションを直接的に利用することもできます。

ただし、データの受け渡しやステータスの管理を行うための仕組みをきちんとアプリケーションレベルで設計する必要があります。MySQLなどのRDSを介してデータ授受を行う場合は、複数のアプリケーションでスキーマを共有したりメンテナンス時のデータの齟齬に気を付ける必要があります。S3などのファイルストレージを利用する場合は、シンプルな構造にしやすいですが、jsonなどのファイルに情報を埋め込む場合は同様に拡張性やメンテナンス性を考えて仕様を検討する必要があります。

時間のかかるバッチ処理の場合は、インフラ部分の比重が高い為、大掛かりなシステム以外では導入の検討が慎重に行う必要があります。手軽になってきたとはいえ、Laravel workerを使ってローカル環境でテストを行うことが出来れば、本番のクラウド環境でも再現しやすいのでまずはLaravel workerを使用して実装することを心がけています。

まとめ

何となく実装しがちな非同期処理を、目的別に振り分けてみました。非同期処理はデバッグの難易度が上がるとともに、予想外の要件やバグが出てくることもあるので、慎重に実装する必要があります。Laravelを使うことによって手軽に実装することが出来るようになっていますが、本質的な要件を理解したうえで、適切な方法を選択する必要があります。