PHP-процесс живёт между запросами: как перейти с PHP-FPM на FrankenPHP и не получить утечку памяти

Ускорить Laravel-приложение без переписывания звучит заманчиво: ставим FrankenPHP, подключаем Octane — и не загружаем фреймворк при каждом запросе. Но это уже не простая замена веб-сервера, а смена модели выполнения кода.

Процесс PHP-FPM тоже обслуживает много запросов, однако приложение обычно загружается заново, а состояние запроса очищается. В worker mode FrankenPHP запускает приложение один раз и оставляет его в памяти. Это сокращает расходы на bootstrap, но открывает ошибки, которые раньше исчезали после запроса.

Поэтому правильный вопрос звучит не «насколько FrankenPHP быстрее PHP-FPM», а «готово ли приложение жить дольше одного запроса».

Что неожиданно остаётся в памяти

В долгоживущем процессе сохраняются статические свойства и переменные, глобальные объекты, самодельные in-memory-кеши и сервисы, зарегистрированные как singleton. Если такой объект однажды получил данные пользователя, текущий Request или устаревшее соединение, следующий запрос может увидеть состояние предыдущего.

Простейший пример:

    final class RequestLog
    {
        public static array $items = [];
    }

    RequestLog::$items[] = $request->user()?->id;

Под PHP-FPM подобный код мог годами не показывать проблему. В worker mode массив будет расти, пока процесс не перезапустится. Это одновременно утечка памяти и риск смешивания данных разных пользователей.

Laravel Octane сбрасывает известное фреймворку состояние, но не может понять назначение каждого singleton или глобальной переменной. Проверьте:

  • сервисы, получающие Request, контейнер или изменяемую конфигурацию;
  • static-массивы, глобальные реестры и безразмерные кеши;
  • подключения к БД, Redis и API после простоя или обрыва;
  • библиотеки, не освобождающие файлы, дескрипторы и большие объекты;
  • код, меняющий $_ENV, часовой пояс или locale.

Опасен не сам singleton, а объект, который живёт дольше хранящихся в нём данных.

Ограничение числа запросов — страховка, а не лечение

Octane умеет мягко перезапускать worker после заданного числа запросов. В актуальной документации значение по умолчанию — 500, а изменить его можно параметром:

    php artisan octane:start --server=frankenphp --max-requests=250

У FrankenPHP worker mode есть аналогичная возможность. Она полезна, потому что часть старого PHP-кода и библиотек всё ещё накапливает память. Но уменьшать max_requests, пока график RSS перестал пугать, — плохая диагностика. Перезапуск ограничит последствия, но не устранит утечку и может скрыть её до роста трафика.

Прогрейте приложение одинаковой последовательностью запросов и наблюдайте за каждым worker: растёт ли RSS, возвращается ли память после тяжёлого запроса, что происходит после ошибок API. Записывайте p95, CPU, 5xx и соединения с базой. Синтетический бенчмарк ничего не говорит об авторизации, корзине и реальных запросах к БД.

Сколько workers запускать

Правило «один worker на ядро» — только отправная точка. Слишком мало workers создаст очередь, слишком много — конкуренцию за CPU, память и соединения с БД.

Для предварительной оценки можно использовать консервативную схему:

RAM сервера ≈ ОС + proxy + БД/Redis + (RSS worker × workers) + запас 20–30%

RSS измеряйте после прогрева. Если база и Redis находятся на том же VPS, учитывайте их пик отдельно. Сопоставьте workers с лимитом подключений к БД: ускоренный веб-слой легко переносит узкое место ниже по стеку.

Деплой: новый код сам не подхватится

В PHP-FPM разработчик привыкает, что заменённый файл начинает работать почти сразу. Долгоживущий worker продолжит выполнять уже загруженный код. После переключения релиза Octane нужно перезагрузить:

    php artisan octane:reload

Очереди Laravel — такие же долгоживущие процессы. Для них используется:

    php artisan queue:restart

Команды должны быть частью deploy-скрипта. Нужен process manager — Supervisor, systemd или политика контейнера, — который поднимет завершившиеся процессы. Перезагрузка выполняется после переключения релиза, иначе часть workers останется на старом коде.

Миграции БД делайте совместимыми с обеими версиями приложения: возврат на PHP-FPM не поможет, если прежнему коду уже не хватает удалённой колонки.

Как проверить переход без ставки на production

FrankenPHP имеет смысл сначала поднять в отдельном staging-контуре с той же версией PHP, расширениями, конфигурацией приложения и близкими настройками базы. Для функциональных тестов сервер может быть скромнее production. Для сравнения производительности нужны сопоставимые CPU, RAM и хранилище — иначе измеряется разница машин, а не режимов PHP.

Скопированную базу обезличьте, используйте отдельные ключи API, отключите платежи и письма, закройте staging авторизацией. Тестируйте реальные сценарии и сравнивайте не только requests per second, но и p95, ошибки, RSS и нагрузку на БД.

Для постоянного staging-контура под такой эксперимент у xHost24 доступны KVM VPS с NVMe, Linux-шаблонами и собственным ISO, а для ресурсоёмких схем — линейка Power VPS. Конфигурацию стоит выбирать по замерам приложения; snapshots подключаются отдельно и не заменяют внешние резервные копии. Если сервер нужен на длительный срок, условие 6=12 необходимо подтвердить в тикете до оплаты.

На production оставьте быстрый путь назад: прежний upstream PHP-FPM и проверенный rollback. Если возможно, начните с части трафика и заранее задайте пороги возврата по 5xx, p95, RSS и состоянию БД.

Короткий чек-лист перед переключением

  • нет пользовательских данных в static, globals и долгоживущих singleton;
  • проверены пакеты, соединения с БД, Redis и внешними API;
  • явно заданы число workers, max_requests и лимиты памяти;
  • настроен process manager и автоматический запуск после reboot;
  • deploy перезагружает Octane и очереди;
  • мониторинг показывает p95, 5xx, CPU, RSS и состояние базы;
  • есть отдельный staging, резервная копия и реально проверенный rollback на PHP-FPM.

FrankenPHP и Octane способны убрать повторный bootstrap и дать приложению больше пропускной способности, но бесплатного ускорения здесь нет. Worker mode меняет жизненный цикл программы. Если команда понимает, какое состояние сохраняется, измеряет память и умеет перезагружать процессы без потери запросов, переход становится управляемым. Если нет — стабильный PHP-FPM окажется быстрее любой ночной аварии.

loader
Комментарии
Новый комментарий

Логические задачи с собеседований