PHP-FPMを使った非同期処理

Laravel

2024-11-16

はじめに

ウェブアプリケーションのパフォーマンス向上やUXの改善の手段として、非同期処理があります。

一般的に、Laravelで非同期処理を行う場合、ジョブ・キューを利用し、ワーカーを起動する環境を用意します。

これには専用のサーバーやプロセスの管理が必要で、実装コストが高くなる傾向があります。

より手軽に非同期処理を実現する方法を探していたところ、PHP-FPMのfastcgi_finish_request関数を利用することで簡易的に非同期処理を実現できることを知りました。

本記事では、その実装方法や使用時の注意点をまとめます。

前提条件

WebサーバーにはNginxを利用し、アプリケーションサーバーにはPHP-FPMを利用します。LaravelはPHP-FPM上で動作させる想定です。

各バージョンは以下のとおりです。

非同期処理のフロー

以下は、今回想定しているPHP-FPMを介してfastcgi_finish_request関数を使った際の、非同期処理フローです。

ポイントは、fastcgi_finish_request 関数を使うことで、レスポンスを返しつつも、PHP-FPMはバックグラウンドで処理を継続している点です。

画像の説明: php-fpm-async-processing-flow-image-moun8fv9d3ahR5FCOso2Ibo597etaI.png

実装方法

実装方法と言っても、特に自前で何かコードを書く必要はありません。

LaravelのHTTPレスポンス処理の基本機能を提供しているSymfony\Component\HttpFoundation\Response クラスのsendメソッドを使うことで、PHP-FPM上で動いている場合は自動的にfastcgi_finish_request 関数を実行してくれます。

public function send(/* bool $flush = true */): static
{
    $this->sendHeaders();
    $this->sendContent();

    $flush = 1 <= \func_num_args() ? func_get_arg(0) : true;
    if (!$flush) {
        return $this;
    }

    if (\function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    } elseif (\function_exists('litespeed_finish_request')) {
        litespeed_finish_request();
    } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
        static::closeOutputBuffers(0, true);
        flush();
    }

    return $this;
}

LaravelのIlluminate\Http\Response クラスはSymfony\Component\HttpFoundation\Response を継承しているため、以下のように簡単に非同期処理を行うことができます。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class HogeController extends Controller
{
    public function hoge(Request $request): JsonResponse
    {
        // レスポンス用データの生成処理
        $responseData = $this->prepareResponseData();
        // レスポンスを生成
        $response = response()->json($responseData);

        // sendメソッドを呼び出すことでレスポンスを即時返却
        $response->send();
        
        // 非同期処理実行
        $this->startAsyncProcess();
    }
}

注意点

レスポンスを返す際に実行されるミドルウェアが実行されない

通常のレスポンスの場合、レスポンスを返す処理の中でミドルウェアを通過します。

ただ、fastcgi_finish_request 関数を実行して、レスポンスを即時に返すとミドルウェアは通過しません。

そのためレスポンスヘッダや、ミドルウェアでterminate メソッドを実装している場合は手動で実行させる必要があります。

public function hoge(Request $request): JsonResponse
{
    // レスポンス用データの生成処理
    $responseData = $this->prepareResponseData();
    // レスポンスを生成
    $response = response()->json($responseData);
    // CORSエラー回避用ヘッダーを付与
    $response->header('Access-Control-Allow-Origin', config('app.front_url'));
    $response->header('Access-Control-Allow-Credentials', 'true');
        
    // sendメソッドを呼び出すことでレスポンスを即時返却
    $response->send();
        
    // レスポンス返却後の後処理を実行
    $kernel = app(\App\Http\Kernel::class);
    $kernel->terminate($request, $response);
        
    // 非同期処理実行
    $this->startAsyncProcess();
}

非同期処理時に発生したエラーに気づきづらい

処理に失敗しても、リクエストを投げたクライアントには通知されないため、エラーに気づきにくいです。

エラーや例外をキャッチしてログに記録するよう、エラーハンドリングを徹底する必要があります。

fastcgi_finish_request 関数はタイムアウトがかからない

PHP-FPMのデフォルトの設定では、fastcgi_finish_request 関数実行後はタイムアウトがかかりません。そのため、処理がいつまでも終了しないという状況も起こり得ます。

また、PHP の設定値 max_execution_time も効かないので注意が必要です。

PHP-FPMの設定次第ではタイムアウトを設けることは可能です。

php-fpm.conf やプール設定ファイルに以下の設定を追加します。

request_terminate_timeout_track_finished = yes
request_terminate_timeout = 300

まとめ

アプリケーションサーバーにPHPーFPMを使っている場合、fastcgi_finish_request関数を活用することで、Laravelアプリケーションで手軽に非同期処理を実現できます。

ただ、Laravelのライフサイクルからズレる処理になってしまうので、利用には色々と注意が必要です。

ジョブ・キューを使って非同期処理を行うのが一番ですが、手軽に非同期処理を実現したい場合には最適です。

利用シーンは限定的かもしれませんが、参考にしていただければ幸いです。