Laravel Octane

laravel octane

Исследуем новый пакет Тейлора Отвелла и расскажем как его установить, когда использовать и вообще зачем он может понадобиться.

Что такое Laravel Octane?

Это пакет с открытым исходным кодом, который существенно повысит производительность вашего Laravel-приложения. Он требует PHP 8, поэтому, если вы все еще используете 7.x, то вам необходим апгрейд языка.
Под капотом Octane использует Swoole и RoadRunner — два сервера приложений, которые позаботятся об обслуживании и загрузке вашего приложения. Вы можете спросить, как это ускорит работу? Сейчас расскажу.

В обычном PHP-приложении, которое работает, например, через веб-сервер nginx, каждый входящий запрос будет порождать PHP-FPM воркер. Это означает, что каждый запрос запускает отдельный процесс PHP, выполняющий все необходимые задачи для обслуживания этого запроса.

В случае Laravel это означает, что загрузится весь фреймворк, все сервис-провайдеры зарегистрируют свои службы, загрузятся провайдеры, запрос пройдет через список мидлваров, попадёт в контроллер, затем отрендерится шаблон и т. д., пока мы не получим ответ от сервера.

При наличии Swoole или RoadRunner у нас по-прежнему будет воркер для каждого входящего HTTP-запроса, но все воркеры используют один и тот же уже загруженный фреймворк. Это означает, что только первый входящий запрос подгрузит фреймворк (включая всех сервис-провайдеров и т. д.), в то время как каждый последующий запрос будет использовать уже загруженный. И это именно то, что делает Octane очень быстрым.

Начинаем работать с Laravel Octane

Поскольку это пакет, то необходимо его установить через композер:

composer require laravel/octane

После установки нужно обязательно запустить команду php artisan octane:install. Будет опубликован файл конфигурации, а также добавлен бинарник RoadRunner  rr в файл .gitignore.

Файл конфигурации выглядит так:

return [

    /*
    |--------------------------------------------------------------------------
    | Octane Server
    |--------------------------------------------------------------------------
    |
    | This value determines the default "server" that will be used by Octane
    | when starting, restarting, or stopping your server via the CLI. You
    | are free to change this to the supported server of your choosing.
    |
    */

    'server' => env('OCTANE_SERVER', 'roadrunner'),

    /*
    |--------------------------------------------------------------------------
    | Force HTTPS
    |--------------------------------------------------------------------------
    |
    | When this configuration value is set to "true", Octane will inform the
    | framework that all absolute links must be generated using the HTTPS
    | protocol. Otherwise your links may be generated using plain HTTP.
    |
    */

    'https' => env('OCTANE_HTTPS', false),

    /*
    |--------------------------------------------------------------------------
    | 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,
        ],

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

        RequestHandled::class => [
            //
        ],

        RequestTerminated::class => [
            //
        ],

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

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

        OperationTerminated::class => [
            FlushTemporaryContainerInstances::class,
            // DisconnectFromDatabases::class,
            // CollectGarbage::class,
        ],

        WorkerErrorOccurred::class => [
            ReportException::class,
            StopWorkerIfNecessary::class,
        ],

        WorkerStopping::class => [
            //
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Warm / Flush Bindings
    |--------------------------------------------------------------------------
    |
    | The bindings listed below will either be pre-warmed when a worker boots
    | or they will be flushed before every new request. Flushing a binding
    | will force the container to resolve that binding again when asked.
    |
    */

    'warm' => [
        ...Octane::defaultServicesToWarm(),
    ],

    'flush' => [
        //
    ],

    /*
    |--------------------------------------------------------------------------
    | Garbage Collection Threshold
    |--------------------------------------------------------------------------
    |
    | When executing long-lived PHP scripts such as Octane, memory can build
    | up before being cleared by PHP. You can force Octane to run garbage
    | collection if your application consumes this amount of megabytes.
    |
    */

    'garbage' => 50,

    /*
    |--------------------------------------------------------------------------
    | Maximum Execution Time
    |--------------------------------------------------------------------------
    |
    | (info) 0 means no maximum limit
    |
    */

    'max_execution_time' => 30,

    /*
    |--------------------------------------------------------------------------
    | Octane Cache Table
    |--------------------------------------------------------------------------
    |
    | While using Swoole, you may leverage the Octane cache, which is powered
    | by a Swoole table. You may set the maximum number of rows as well as
    | the number of bytes per row using the configuration options below.
    |
    */

    'cache' => [
        'rows' => 1000,
        'bytes' => 10000,
    ],

    /*
    |--------------------------------------------------------------------------
    | Octane Swoole Tables
    |--------------------------------------------------------------------------
    |
    | While using Swoole, you may define additional tables as required by the
    | application. These tables can be used to store data that needs to be
    | quickly accessed by other workers on the particular Swoole server.
    |
    */

    'tables' => [
        'example:1000' => [
            'name' => 'string:1000',
            'votes' => 'int',
        ],
    ],

];

Затем вам нужно решить, что вы будете использовать: RoadRunner или Swoole. Для этого служит ключ server, который может принимать значения swoole либо roadrunner.

RoadRunner

RoadRunner — это сервер приложений, написанный на Go, который не имеет других зависимостей в PHP. Выбирайте RoadRunner, если вы не хотите устанавливать дополнительные PHP-расширения. Можно установить RoadRunner, например, через композер:

composer require spiral/roadrunner

Swoole

Swoole имеет несколько преимуществ, которых нет у RoadRunner. Поскольку Swoole является расширением поверх языка, то PHP получает дополнительные новые фичи: ticks (такты) и coroutines (сопрограммы), о которых я расскажу немного позже. Они недоступны в RoadRunner, поэтому, если вы хотите их использовать, то вам следует использовать именно Swoole.

Так вы можете установить Swoole:

pecl install swoole

Во время установки вас спросят, нужна ли вам поддержка HTTP2, curl, JSON и open_ssl. Спокойно используйте дефолтные значения (то есть off), поскольку они влияют только на coroutines. Вы по-прежнему сможете использовать curl, JSON и все остальное.

Запускаем Octane

После того, как вы установили RoadRunner или Swoole и настроили их в своем файле octane.php, вы можете запустить Octane и позволить ему обслуживать ваше приложение. Сервер Octane запускается командой:

php artisan octane:start

По умолчанию Octane запускает сервер на порту 8000, поэтому вы можете получить доступ к своему приложению в браузере через http://localhost:8000.

Попробуйте и посмотрите, как летает ваше приложение! Если вы сделаете несколько запросов, то увидите, что первый из них немного медленнее — именно в нем происходит загрузка фреймворка, в то время как другие заметно быстрее, так как используют уже загруженный фреймворк прямо из памяти.

200 GET / .............. 14.23 ms
200 GET / ..............  1.93 ms
200 GET / ..............  2.06 ms

Внесение изменений в код

Если вы сейчас пойдете и поменяете свой код, например, добавите новый маршрут /test, то при попытке использовать его получите ошибку 404. Ведь Octane использует фреймворк (и все маршруты) таким, каким он был в момент запуска Octane-сервера. Соответственно, чтобы увидеть изменения кода, необходимо перезапустить сервер. Поскольку достаточно утомительно это делать во время разработки, то
Octane предлагает удобный способ отслеживания изменений кодовой базы и сам автоматически перезапускает сервер.

Чтобы это заработало, не забудьте установить Chokidar — библиотеку на базе NodeJS:

npm install --save-dev chokidar

Затем запустим Octane-сервер в режиме «наблюдения»:

php artisan octane:start --watch

Теперь, когда вы измените свой код, то Octane это обнаружит, перезапустит воркеры и вы сразу увидите все свои изменения.

Настройка воркеров

Кстати, о воркерах — по умолчанию Octane запускает по одному воркеру на каждое ядро вашего процессора. Но можно изменить это поведение параметром --workers команды octane:start:

php artisan octane:start --workers=2

Особенности Swoole

Как я уже упоминал, у Swoole есть несколько фишек. Давайте изучим их подробнее.

Параллельные задачи

Octane позволяет выполнять несколько задач одновременно. Это означает, что они начнут выполняться в одно и то же время и результат будут возвращен, как только все задачи будут выполнены.

Вот пример из GitHub-документации Octane:

use App\User;
use App\Server;
use Laravel\Octane\Facades\Octane;

[$users, $servers] = Octane::concurrently([
    fn () => User::all(),
    fn () => Server::all(),
]);

В этом примере мы получаем всех пользователей и все серверы одновременно. Чтобы было понятнее, вот еще один пример:

Octane::concurrently([
      function () {
        	sleep(0.5);
        	return 1;
      },
      function () {
        	sleep(1);
        	return 2;
      },
  ]);

Мы выполняем две задачи одновременно, и PHP продолжит выполнение, как только обе задачи будут выполнены. Одна задача ждет 0,5 секунды, другая — 1 секунду. Поскольку они выполнятся одновременно в двух разных потоках, то PHP будет ждать ровно 1 секунду (а не 1,5), до получения ответа. Эта фишка отличный способ одновременно выполнять несколько небольших задач.

Как и в случае с --workers, вы также можете настроить количество --task-workers.

Такты / Интервалы

Octane в сочетании с Swoole позволяет регистрировать ticks (такты) — операции, которые будут автоматически выполняться с заданным интервалом. Аналогично методу setInterval в JavaScript. К сожалению, на данный момент нет способа остановить такты. Активировать их можно следующим образом:

Octane::tick('simple-ticker', fn () => ray('Ticking...'))
        ->seconds(10);

Кэш Octane

Еще одна новая фишка Octane и Swoole — новый драйвер кеша. Согласно официальной документации, он обеспечивает скорость чтения и записи до 2 миллионов операций в секунду. Под капотом Swoole кэширует данные в общей памяти с помощью таблиц Swoole, к которым могут получить доступ все воркеры. Однако при перезапуске сервера кэшированные данные будут потеряны, поскольку кеш сохраняется только в памяти.

Доступ к этому кэшу можно получить через Cache-фасад:

Cache::store('octane')->put('framework', 'Laravel', 30);

Еще одна интересная фишка, специфичная для Swoole и Octane — возможность «интервала кеширования». Это позволяет хранить информацию в кеше Octane и обновлять данные в заданном интервале:

use Illuminate\Support\Str;

Cache::store('octane')->interval('random', function () {
    return Str::random(10);
}, seconds: 5)

Таблицы Octane

С помощью Swoole Tables вы можете создавать свои собственные таблицы, к которым хотите получить доступ в своих Octane-приложениях. У них такое же преимущество в производительности, как и у кэша и они позволяют сохранять данные в структурированном виде. Имейте в виду, что все данные, которые вы храните в такой таблице, будут потеряны при перезапуске сервера.

Чтобы настроить таблицу создайте запись в разделе tables конфигурационного файла octane.php:

'tables' => [
    'example:1000' => [
        'name' => 'string:1000',
        'votes' => 'int',
    ],
],

В этом примере мы задаем таблицу под названием example, которая может содержать максимум 1.000 записей/строк. Структура этой таблицы представляет собой поле name — строка с максимальной длиной 1000 и поле votes, хранящее целые числа.

Чтобы записать данные в эту таблицу, мы можем использовать метод Octane::table:

use Laravel\Octane\Facades\Octane;

Octane::table('example')->set('a-unique-identifier', [
    'name' => 'Marcel Pociot',
    'votes' => 1000,
]);

Для получения данных используется метод get:

return Octane::table('example')->get('a-unique-identifier');

Предостережения

Есть несколько вещей, на которые вам нужно обратить внимание при использовании пакета.

Поскольку Octane хранит фреймворк в памяти для всех воркеров, то такие вещи, как например сервис-провайдеры, будут зарегистрированы и загружены только один раз. И хотя Octane заботится о сбросе состояния основных пакетов (включая Inertia), но он не может сбросить глобальное состояние, которое возможно есть в коде вашего приложения.

Официальная документация, которую есть на GitHub, содержит некоторые из наиболее распространенных сценариев, на которые следует обратить внимание.

Слушатели

Одна из особенностей Octane, которая еще не задокументирована — возможность регистрировать кастомных слушателей. Вы можете подключиться к следующим событиям:

  • WorkerStarting (старт воркера)
  • RequestReceived (получение запроса)
  • RequestHandled (обработка запроса)
  • RequestTerminated (отмена запроса)
  • TaskReceived (получение задачи)
  • TickReceived (получение такта)
  • OperationTerminated (отмена операции)
  • WorkerErrorOccurred (ошибка воркера)
  • WorkerStopping (воркер остановлен)

Чтобы прикрепить слушателей к этим событиям, добавьте их в конфигурационный файл octane.php.

Прогрев и сброс сервисов

Когда загружается новый воркер, вы можете указать список привязок/сервисов, которые вы хотите «разогреть» во время процесса загрузки. Это означает, что при загрузке воркера сервис-контейнер уже сделает указанные сервисы доступными и последующие запросы могут сразу получить к ним доступ.

В Octane уже есть список внутренних сервисов, которые разогреваются во время загрузки каждого воркера, но вы можете добавить и свои сервисы в раздел warm файла octane.php.

Также вы также можете задать список сервисов, которые вы хотите сбрасывать, перед новым запросом. Это полезно для сервисов, которые изменются во время запроса и их необходимо каждый раз приводить к исходному состоянию.

Маршруты Octane

Из пакета можно выжать еще немного производительности, если использоваться встроенную маршрутизацию. Вы можете задать свой собственный Octane-маршрут через фасад следующим образом:

Octane::route('GET', '/fast', function() {
    
});

Эти маршруты очень быстрые, так как что они полностью игнорируют систему маршрутизации фреймворка (поэтому эти маршруты не предоставляют никаких мидлваров), что может быть полезно для конечных точек, которые должны очень быстро выдать данные.

Поскольку для этих запросов не используется ларавельный HTTP Kernel, то вам нужно самостоятельно вернуть объект Symfony Response, например так:

use Symfony\Component\HttpFoundation\Response;

Octane::route('GET', '/faster', function() {
    return new Response('Hello from Octane.');
});

Все сервис-провайдеры, разумеется, загружены и доступны, так что вы по-прежнему можете использовать их, выполнять Eloquent-запросы и т. д.

Так почему Октан?

Laravel Octane определенно даст вашему Laravel-приложению огромный прирост производительности, а мы все очень любим когда всё работает очень быстро, не так ли? Действительно ли вам нужна такая скорость? Это зависит от целей вашего приложения. Но для меня важнее то, что Laravel (в который раз) форсирует текущее состояние PHP. Octane это не только пакет который требует PHP 8, но он также предлагает новые захватывающие особенности языка, такие как coroutines и ticks.

Автор: Marcel Pociot
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.