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 окажется быстрее любой ночной аварии.

Комментарии