Laravel Octaneの開発時のハマりポイントと回避方法について


本記事は、サムザップ Advent Calendar 2021の12/12の記事です。

はじめに

Laravel Octane(Swoole版)でアプリケーションをつくるときにはまったポイントを紹介していきます

ハマりポイントをすぐ知りたい方は、ローカルの開発環境構築はすっとばしてください(サンプルアプリケーションの構築から読んでください)

ローカルの開発環境構築

基本は公式ドキュメントにしたがって構築していきます
今回は、Laravel Sailを使う方法を選択しています

Laravel アプリケーションの作成

まずはLaravelのアプリケーションを作成します

$ composer create-project laravel/laravel sample

Laravel Octaneのインストール

つづいてLaravel Octaneのインストール

$ cd sample
$ composer require laravel/octane

Laravel Sailのインストール

つづいてLaravel Sailのインストール

$ composer require laravel/sail --dev
$ php artisan sail:install

使用するミドルウェアの選択肢がでてきます
今回は、MySQLだけを使用するので、0を選択します

 Which services would you like to install? [mysql]:
  [0] mysql
  [1] pgsql
  [2] mariadb
  [3] redis
  [4] memcached
  [5] meilisearch
  [6] minio
  [7] mailhog
  [8] selenium
>0

ドキュメントに従い一度、buildします

$ ./vendor/bin/sail build --no-cache

supervisor.confファイルを調整するためにPublishします

$ ./vendor/bin/sail up
$ ./vendor/bin/sail artisan sail:publish

※ コンテナ名を変更している場合は、環境変数 APP_SERVICE に変更したコンテナ名を渡してください
変更しないと、 sail:publish で使用するコンテナ名の解決に失敗するため

カレントディレクトリ配下に docker ディレクトリが作成されます

sailがつかう docker-compose.yml にて

docker-compose.yml
context: ./docker/8.0

になっていることを確認します(PHP8.0を使います)

supervisord.confを編集して、Swooleを使うようにします

$ vi docker/8.0/supervisord.conf

commandの行を書き換えます

変更前

supervisord.conf(変更前)
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80
user=sail

変更後

supervisord.conf(変更後)
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000

serveoctane:start --server=swoole に変更しています

もう一度コンテナイメージを作り直します

$ ./vendor/bin/sail build --no-cache

Laravel OctaneのSwooleモードでは、Port 8000番をListenするように変更したので、それにあわせて、 docker-compose.yml も修正します

docker-compose.yml
        ports:
            - '${APP_PORT:-80}:80'
            - 8000:8000

8000番を追加します

Laravel Octaneの起動

Laravel OctaneをSwooleモードで起動します

$ ./vendor/bin/sail up

起動時のメッセージでLarave Octaneが8000番をListenしているのが確認できます

sample-laravel.sample-1  | 2021-11-20 09:28:47,887 INFO Set uid to user 0 succeeded
sample-laravel.sample-1  | 2021-11-20 09:28:47,891 INFO supervisord started with pid 16
sample-laravel.sample-1  | 2021-11-20 09:28:48,897 INFO spawned: 'php' with pid 17
sample-laravel.sample-1  | 2021-11-20 09:28:49,899 INFO success: php entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
sample-laravel.sample-1  | 
sample-laravel.sample-1  |    INFO  Server running…
sample-laravel.sample-1  | 
sample-laravel.sample-1  |   Local: http://0.0.0.0:8000 
sample-laravel.sample-1  | 
sample-laravel.sample-1  |   Press Ctrl+C to stop the server
sample-laravel.sample-1  | 

最後にoctaneのconfigファイルを生成します

$ ./vendor/bin/sail artisan octane:install
 Which application server you would like to use?:
  [0] roadrunner
  [1] swoole
 >1

今回はSwooleなので1を選択します
これにより、config/octane.php が生成されます

サンプルアプリケーションの構築

次のディレクトリ構成でファイルを作成します

Sample
├── app
│   ├── Console
│   ├── Exceptions
│   ├── Http
│   │   ├── Controllers
│   │   │   ├── Controller.php
│   │   │   └── SampleController.php   新規作成
│   │   ├── Kernel.php
│   │   └── Middleware
│   ├── Models
│   ├── Providers
│   └── Service
│       └── RandomNumberService.php 新規作成
├── artisan
├── bootstrap
├── composer.json
├── config
├── database
├── docker-compose.yml
├── package.json
├── phpunit.xml
├── public
├── resources
├── routes
│   ├── api.php
│   ├── channels.php
│   ├── console.php
│   └── web.php   変更
├── server.php
├── storage
├── tests
└── vendor

それぞれのファイルは次のように記述しています

(1)コントローラー

SampleController.php
<?php

namespace App\Http\Controllers;

use App\Service\RandomNumberService;
use Illuminate\Http\Response;

class SampleController extends Controller
{
    public function __construct(
        private RandomNumberService $service
    ){}

    public function index(): Response
    {
        return response('Number:' . $this->service->getNumber(), Response::HTTP_OK);
    }
}

(2)サービス

RandomNumberService.php
<?php
namespace App\Service;

class RandomNumberService
{
    protected $number;

    public function __construct()
    {
        $this->number = mt_rand(1, 10000);
    }

    public function getNumber() :int
    {
        return $this->number;
    }    
}

(3)ルーティング

web.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/sample', 'App\Http\Controllers\SampleController@index');

動作確認

実際に 「 http://127.0.0.1:8000/sample 」にブラウザでアクセスしてみると
次のように表示されます(初回表示)

ハマりポイント

ここでブラウザをリロードしてみましょう

プログラムの挙動として、乱数で生成された数字を表示しようとするため、前回と違う数字が表示されることが期待されます

が、実際には、次のように表示されます(2回目以降の表示)

かわっていない!

Laravel Octaneの挙動

通常のPHPはリクエストごとに(リクエストが終わるごとに)状態が破棄されますが、Laravel Octaneは、自身がWebサーバーとなり常時PHPのプログラムがうごいたままになっています
つまり、サンプルアプリケーションの記述の仕方では、コントローラーのインスタンスの生成が1回しかされず、そのまま使いまわされます(2回目以降のリクエストは、生成済みのコントローラーのインスタンスがレスポンスを返している)

はまりました・・・

解決案(1)

Workerに注目する

実は、Laravel OctaneのSwoole版には起動オプションがいくつかあります
その一つが max-requests のオプションです
Laravel Octaneは厳密に言うとWorkerがリクエストの処理を担います
1つのWorkerが何回もリクエストを捌くことになるのですが、Worker自身をリロードする契機としてこの max-reqeust があります
ここに指定した回数のリクエストを処理すると、Workerはリロードします

max-requests=1 にする

では、起動オプションで max-requests を1にしてみましょう

supervisord.confを編集します

supervisord.conf
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=8000 --max-requests=1

sail build でコンテナイメージは作り直します)
では実際の挙動はどうなるでしょうか?

ブラウザでアクセスしてみます

かわった!
けど、おそい!
max-request を1に変更する前は、1回目のリクエスト以外は、50ms以内にレスポンスがかえってきました
しかし、max-request を1に変更したあとのレスポンスは、概ね1000msです
(手元のPCでの計測です)
これは、Laravel Octaneをつかっている意味がなさそうです

解決案(2)

Closureに注目

Laravelのルーターは、第二引数にClosureをとることができます
これを利用します

つまり、ルーターを次のようにします

web.php(追記)
Route::get('/sample', 'App\Http\Controllers\SampleController@index');
Route::get('/sample2', fn () => app()->make(\App\Http\Controllers\SampleController::class)->index());

では、実際に sample2 にアクセスしてみましょう
(画像は省略します)

リロードするたびに値が変わるのが確認できます!
しかもレスポンスも、1回目のリクエスト以外は、50ms以内にレスポンスがかえってきます

解決案(3)(未解決)

Octaneの作法

上記以外にも、Laravel Octaneで用意している仕組みがあります

config/octane.php に次のセクションがあります

octane.php
    /*
    |--------------------------------------------------------------------------
    | Octane Listeners
    |--------------------------------------------------------------------------
    |
    | All of the event listeners for Octane's events are defined below. These
    | listeners are responsible for resetting your application's state for
    | the next request. You may even add your own listeners to the list.
    |
    */

    'listeners' => [
        WorkerStarting::class => [
            EnsureUploadedFilesAreValid::class,
            EnsureUploadedFilesCanBeMoved::class,
        ],

        RequestReceived::class => [
            ...Octane::prepareApplicationForNextOperation(),
            ...Octane::prepareApplicationForNextRequest(),
            //
        ],

RequestReceived::class にて、リクエストを受け付けた時に処理をはしらせるhookの記述があります
ここのhookがつかえそうです

が、コントローラーをうまく初期化する方法まで調べることができませんでした
もしご存知の方がいれば、情報下さい!

まとめ

Laravel Octaneでアプリケーションを作った際に、最初に、コントローラーのコンストラクタが1回しか呼ばれない現象になやまされました
よくよく考えれば、わかることなのですが、その回避策をいくつか検討してみました
現時点では、Closureを使う方法で回避することができました

しかし、よりOctaneらしい記述があれば、そちらを踏襲していければと思います

明日は@norimatsu_yusukeの記事です!