Интеграция с фреймворками
В режиме Classic приложение на фреймворке работает без изменений. Настройте Rapira для использования существующего входного скрипта. В режиме Worker процесс PHP остаётся активным между запросами. Архитектура фреймворка определяет, какое состояние приложения может оставаться в памяти. Эта страница описывает правила для всех фреймворков. Руководства по фреймворкам описывают только особенности конкретного фреймворка.
Проверено на
- PHP 8.5.8, NTS, embed SAPI.
- Rapira 0.8.0.
- Symfony 7.4.15 и 8.1.2, шаблон приложения Yii3 1.4 (yii-runner-http 3.2.1).
Тесты запускали эти приложения в Linux с одним процессом воркера. Утверждения о фреймворках на этой странице основаны на этих тестах. Примеры на этой странице используют формат конфигурации v0.9. Параметры Rapira описаны в разделе Конфигурация.
Режимы Classic и Worker
В режиме Classic используется существующий входной скрипт. Для каждого HTTP-запроса Rapira запускает новый запрос PHP. Фреймворк, который работает под php-fpm, также работает в этом режиме. Дополнительная информация находится на странице Режим Classic. Только приведённые ниже разделы о статических файлах, TLS и OPcache относятся к режиму Classic.
В режиме Worker процесс остаётся активным. Скрипт инициализирует приложение и запрашивает работу в цикле. Состояние приложения остаётся между запросами. Дополнительная информация находится в разделах Режимы выполнения и Режим Worker.
Одна кодовая база может использовать оба режима. Оставьте public/index.php. Добавьте worker.php в корневой каталог проекта. Ключ http.pool.mode выбирает режим выполнения, а http.pool.entrypoint выбирает скрипт. Режим Classic остаётся доступным, если переход на режим Worker не удался.
Каждый воркер использует каталог своего входного скрипта как рабочий каталог. Поэтому относительные пути к файлам в worker.php разрешаются относительно корневого каталога проекта. Относительные пути к файлам в public/index.php разрешаются относительно public/. Вызов chdir() в коде PHP действует до завершения процесса воркера.
Цикл Worker
Для каждого фреймворка используется одна базовая структура скрипта воркера:
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';
$app = new App(); // The worker creates this object once and reuses it.
$handler = static function () use ($app): void {
header('Content-Type: text/plain');
http_response_code(200);
echo $app->handle($_SERVER['REQUEST_URI']);
};
while (\Rapira\handle_request($handler)) {
gc_collect_cycles();
}Скрипт выполняет следующие операции:
require .../vendor/autoload.phpрегистрирует автозагрузчик. Автозагрузчик и загруженные классы остаются доступными до перезапуска скрипта воркера.$app = new App();инициализирует приложение перед циклом. Symfony хранит здесь постоянное ядро. Yii3 может хранить постоянный раннер или создавать раннер в обработчике. Каждое руководство содержит инициализацию и очистку запроса.$handler = static function () use ($app): voidопределяет обработчик без аргументов. Обработчик читает данные запроса из суперглобальных переменных. Другие зависимости обработчик получает черезuse.header(),http_response_code()иechoформируют ответ как в классическом скрипте. Передача ответа описана в разделе HTTP.while (\Rapira\handle_request($handler))ожидает запрос.handle_request()заполняет суперглобальные переменные, запускает обработчик и завершает запрос. Функция возвращаетtrueпосле запроса иfalseпри остановке воркера. Вызывайте её только из цикла верхнего уровня скрипта. Вне режима Worker функция бросаетRapira\Exception\NotInWorkerModeError.gc_collect_cycles();собирает циклические ссылки между запросами. Эта функция не исправляет утечки памяти. Дополнительная информация находится в разделе Память и перезапуск воркеров.
Внутри обработчика Rapira задаёт SCRIPT_NAME как /worker.php, потому что worker.php является входным скриптом. DOCUMENT_ROOT содержит каталог скрипта. REQUEST_URI содержит путь клиента. Symfony и Yii3 правильно маршрутизировали запросы и создавали URL с этими значениями. Созданные URL не содержали worker.php. Перед интеграцией другого фреймворка проверьте, строит ли он URL из SCRIPT_NAME вместо REQUEST_URI.
До первого запроса $_SERVER содержит окружение процесса. В этот момент SCRIPT_NAME содержит абсолютный путь скрипта, а DOCUMENT_ROOT пуст. $_SERVER запроса не содержит окружение процесса. Читайте переменные окружения до цикла или используйте getenv() в обработчике. Не вычисляйте префикс URL из $_SERVER до цикла. Дополнительная информация находится в разделе $_SERVER до первого запроса.
Состояние запроса и резидентное состояние
Rapira пересоздаёт всё в левой колонке для каждого запроса. Обычный код PHP может читать эти значения. Всё в правой колонке сохраняется между запросами. Скрипт воркера должен управлять этим состоянием.
| Создаётся заново для каждого запроса | Сохраняется между запросами |
|---|---|
$_GET, $_POST, $_SERVER, $_COOKIE: Rapira заполняет их данными запроса. $_SERVER не содержит окружение процесса | Автозагрузчик Composer и каждый загруженный им класс |
php://input: исходное тело запроса, CONTENT_TYPE и CONTENT_LENGTH | Свойства и переменные static, которые сохраняют значения между запросами |
$_FILES и загруженные временные файлы | Объекты, созданные до цикла, например контейнер, ядро и приложение |
Данные сессии: session_start(), cookie запроса и поле ответа Set-Cookie | Открытые ресурсы: соединения с базой данных, клиенты кеша, потоки |
Состояние ответа: код статуса, заголовки, setcookie() и буферы вывода | Процесс: один pid и один резидентный интерпретатор PHP для каждого воркера |
| Shutdown-функции, зарегистрированные внутри обработчика | Значения $_ENV, загруженные до цикла |
Таймер max_execution_time, который перезапускается для каждого запроса |
Rapira запускает новый таймер max_execution_time для каждого запроса. Время, в течение которого воркер ожидает запрос, не входит в этот лимит.
Следующие три особенности относятся к резидентному воркеру.
Резидентный объект сохраняет состояние между запросами
PHP не вызывает деструктор резидентного объекта в конце запроса. Деструктор выполняется один раз: когда завершается цикл воркера или когда код удаляет последнюю ссылку на объект.
Не используйте деструктор для очистки, которая нужна после каждого запроса. Сбрасывайте состояние запроса внутри обработчика.
Shutdown-функция, зарегистрированная при инициализации, выполняется один раз в конце цикла воркера
Shutdown-функцию, зарегистрированную вне обработчика, PHP выполняет один раз, в конце цикла воркера. Функция, зарегистрированная внутри обработчика, выполняется в конце этого запроса.
Регистрируйте shutdown-функции запроса внутри обработчика. К таким функциям относятся вывод метрик, обработка фатальной ошибки и очистка ресурсов запроса.
$_ENV сохраняется между запросами
Rapira не пересоздаёт $_ENV для каждого запроса. Значения, которые код записывает в $_ENV до цикла, сохраняются до перезапуска скрипта воркера. Загружайте конфигурацию окружения до цикла. Не храните в $_ENV данные запроса.
Изменение $_ENV не изменяет окружение процесса. Используйте putenv(), когда getenv() или дочерние процессы должны видеть значение. В продакшене задавайте переменные окружения в unit-файле, контейнере или оркестраторе.
Обработка ошибок
Тесты подтвердили три типа ошибок с одним воркером:
exitилиdieвнутри обработчика отправляет текущий статус и вывод. Процесс не останавливается, и воркер продолжает принимать запросы. Например, фреймворк может использоватьexitдля ответа о техническом обслуживании.- Непойманное исключение возвращает
500, если PHP не отправил вывод до исключения. После вывода остаётся статус, который PHP уже отправил. Обработчик исключений фреймворка может вернуть свою страницу ошибки. Без такого обработчика и с выключеннымdisplay_errorsтело ответа пустое. Воркер продолжает принимать запросы. - Непойманная
Errorдаёт тот же результат. Для обоих типов PHP записывает в журнал сообщениеUncaught.
Счётчик errors увеличивается, когда ни один обработчик исключений не перехватывает исключение или Error. Запрос с exit увеличивает только handled. Во всех трёх случаях recycles остаётся равным нулю.
Фатальная ошибка класса bailout завершает резидентный скрипт. Затем воркер снова запускает скрипт и инициализирует приложение. Этот перезапуск увеличивает recycles. Страница Модель процессов описывает вывод этих счётчиков.
Статические файлы
Rapira обслуживает статические ресурсы через middleware статических файлов. Задайте для [http.static].root каталог public/ фреймворка. Добавьте middleware в секцию [http]:
[http]
middleware = ["static"]
[http.static]
root = "public"Middleware возвращает ответ, только если путь соответствует файлу в корневом каталоге. Список forbid по умолчанию запрещает файлы .php, поэтому middleware не обслуживает входной скрипт как файл. Для других URL выполняется входной скрипт. Для URL каталогов также выполняется входной скрипт, потому что middleware не обслуживает индексные файлы. $_SERVER['REQUEST_URI'] содержит путь клиента.
Статические ресурсы также может обслуживать CDN или обратный прокси. Конфигурация обратного прокси описана в разделе Запуск в продакшене.
TLS и прокси
Rapira принимает только открытый HTTP и не имеет настроек TLS. Завершайте TLS на прокси. Подключите прокси через петлевой интерфейс или Unix-сокет. $_SERVER['HTTPS'] всегда пуст, а $_SERVER['REQUEST_SCHEME'] всегда равен http. Настройте доверенные прокси фреймворка, чтобы он читал X-Forwarded-Proto. Без этой настройки фреймворк создаёт URL с http://.
Используйте дефисы, а не подчёркивания, в именах перенаправленных полей, потому что оба символа могут соответствовать одному ключу $_SERVER. Дополнительная информация находится в разделах HTTP и Запуск в продакшене.
Память и перезапуск воркеров
Воркер может создавать приложение внутри обработчика. Тогда приложение остаётся в памяти в течение одного запроса. Воркер сохраняет меньше состояния, чем с постоянным ядром Symfony, но больше, чем в режиме Classic. Переносите инициализацию из обработчика только после того, как вы определите постоянное состояние.
В этой схеме каждый запрос создаёт граф объектов. Циклические ссылки могут сохранять старые графы до запуска сборщика циклов. Затем использование памяти растёт в течение нескольких запросов и уменьшается, когда PHP освобождает много графов. Такое поведение не обязательно является утечкой памяти. Однако пиковое использование памяти может значительно превышать память для одного запроса.
Тесты показали, что gc_collect_cycles() в цикле или обработчике не устраняет это поведение. Более поздняя инициализация может сохранять ссылки на старые графы. Сборщик не может освободить граф, пока другой объект ссылается на него. Установите memory_limit выше измеренного максимума. Также установите лимит замены воркера:
[http.pool]
max_requests = 100Каждый воркер останавливается после того, как обслужит больше max_requests запросов, и мастер запускает замену. Каждый воркер использует свой лимит, от max_requests + 1 до max_requests плюс половина этого значения. Поэтому воркеры не останавливаются одновременно. Тесты отправили сотни запросов во время нескольких замен. Память возвращалась к исходному уровню, и каждый запрос возвращал 200. Эта настройка ограничивает пик памяти для этого поведения.
Постоянные приложения Symfony и Yii3 показывали стабильное использование памяти в тех же тестах. Оставьте замену воркеров включённой для ограничения неожиданного роста памяти. Дополнительная информация находится в разделах Конфигурация и Модель процессов.
OPcache и изменившийся код
Rapira запускает PHP один раз в мастер-процессе до создания воркеров. OPcache создаёт один сегмент разделяемой памяти, и каждый воркер наследует это отображение. Скомпилированные скрипты остаются в кеше между запросами и воркерами во всех режимах.
В PHP 8.4 OPcache является отдельным файлом opcache.so, которому нужна строка zend_extension в php.ini. См. php.ini.
В продакшене opcache.validate_timestamps = 0 отключает проверку файлов для каждого запроса. Эта настройка запрещает автоматическую инвалидацию кеша. Сегмент OPcache принадлежит мастер-процессу и сохраняется при замене воркеров. Поэтому после развёртывания требуется полный перезапуск. Последовательность описана в разделе Запуск в продакшене.
Во время разработки постоянное приложение не перечитывает код инициализации. Это поведение не зависит от OPcache. Перезапускайте сервер после изменения скрипта воркера или инициализированных сервисов. Нажмите Ctrl-C, затем снова запустите rapira serve rapira.toml.
Руководства по фреймворкам
- Symfony: ядро инициализируется один раз и остаётся в памяти.
services_resetterсбрасывает сервисы с состоянием между запросами. Один файл воркера поддерживает Symfony 7.4 и 8.1. - Laravel: режим Classic запускает стандартный
public/index.phpбез изменений. Режим Worker находится в разработке: Rapira пока не предоставляет требуемый драйвер Octane. - Yii3:
StateResetterсбрасывает постоянный контейнер после каждого запроса. Также воркер может создавать новый раннер для каждого запроса.
Другие фреймворки могут использовать тот же базовый скрипт воркера. Используйте режим Worker только для приложения, которое может обрабатывать несколько запросов в одном процессе. Сначала создайте приложение внутри обработчика. Эта схема не требует поддержки постоянных процессов фреймворком.
Проверьте приложение в этой схеме. Затем сохраняйте приложение в памяти. Сбрасывайте состояние запроса после каждого запроса. Используйте режим Classic, если ни одна схема Worker не работает правильно.