Режим воркеров
Режим воркеров запускает постоянные PHP-процессы, которые инициализируются один раз, а затем обрабатывают множество запросов, поэтому стоимость запуска PHP оплачивается однократно, а не при каждом запросе. Вместо того чтобы разрушать и заново выстраивать состояние PHP на каждый запрос, ваше приложение загружает автозагрузчик, конфигурацию и подключения к базе данных один раз и переиспользует их на протяжении всего времени жизни воркера.
Как это работает
- Включите режим воркеров. Установите
WORKER_MODE_ENABLED=trueи укажите вENTRY_FILEваш скрипт инициализации. Это включает режим воркеров для всех PHP-воркеров в пуле. - Инициализация один раз. PHP запускается и выполняет внешнюю область видимости один раз. Регистрация автозагрузчика, загрузка конфигурации, подключения к базе данных и любой другой код инициализации выполняются однократно.
- Вход в цикл обработки запросов. Вызовите
oxphp_worker(callback). OxPHP начинает направлять входящие HTTP-запросы в ваш колбэк. - Сброс между запросами. Суперглобальные переменные (
$_GET,$_POST,$_SERVER,$_COOKIE,$_FILES,$_REQUEST,php://input), буферы вывода, заголовки ответа и ini-директивы, изменённые запросом, сбрасываются автоматически — что именно это покрывает и где заканчивается, см. в разделе Что сбрасывается, а что сохраняется. Мягкий сброс очищает состояние отдельного запроса, сохраняя при этом инициализированные ресурсы во внешней области видимости.$_ENV— исключение: она намеренно не сбрасывается — см. Суперглобальные переменные. - Внешняя область видимости сохраняется. Переменные, определённые до
oxphp_worker(), статические свойства, подключения к базе данных и автозагрузчики остаются доступными для всех запросов, обрабатываемых этим воркером.
Режим воркеров меняет поведение маршрутизации. Все запросы, которые не соответствуют статическому файлу на диске, направляются воркеру вместо возврата 404. Подробнее см. Маршрутизация.
Конфигурация
| Переменная | По умолчанию | Описание |
|---|---|---|
WORKER_MODE_ENABLED |
false |
Включает постоянный режим воркеров. Принимает true, 1, yes. Требует, чтобы ENTRY_FILE указывал на .php-скрипт |
ENTRY_FILE |
(не задано) | Путь к скрипту инициализации воркера. Для относительных путей разрешается относительно DOCUMENT_ROOT; сегменты .. и абсолютные пути допускаются (расположение скриптов инициализации воркера за пределами публичного корня документов — поддерживаемая схема) |
WORKER_MAX_MEMORY_MIB |
0 |
Максимальный объём памяти PHP на воркер в МиБ до перезапуска. 0 = без ограничений |
Устаревшая переменная WORKER_FILE по-прежнему разбирается (с выводом WARN при запуске) и ведёт себя как WORKER_MODE_ENABLED=true ENTRY_FILE=$WORKER_FILE. В новых развёртываниях следует использовать явную пару переменных; устаревшая форма будет удалена в одном из будущих релизов.
Для перезапуска, управляемого приложением, вызовите OxPHP\Server\Worker::scheduleExit() внутри обработчика запроса. Воркер завершится корректно после того, как текущий запрос будет обработан.
Написание скрипта воркера
Скрипт воркера состоит из двух частей: внешней области видимости, которая выполняется один раз при запуске, и колбэка, передаваемого в oxphp_worker(), который выполняется при каждом запросе.
<?php
// Outer scope: runs once at startup
require __DIR__ . '/../vendor/autoload.php';
$config = parse_ini_file(__DIR__ . '/../config/app.ini');
$db = new PDO($config['dsn'], $config['user'], $config['pass'], [
PDO::ATTR_PERSISTENT => true,
]);
$app = new MyApp\Application($config, $db);
// Request loop: runs for every request
oxphp_worker(function () use ($app) {
$app->handle();
});
// Shutdown: runs when the worker exits
$app->terminate();Что сбрасывается, а что сохраняется
OxPHP выполняет мягкий сброс между запросами. Состояние отдельного запроса очищается автоматически, тогда как всё, что было инициализировано во внешней области видимости, сохраняется на протяжении всего времени жизни воркера.
- Суперглобальные переменные —
$_GET,$_POST,$_SERVER,$_COOKIE,$_FILESиphp://inputзаполняются заново данными нового запроса - Буферы вывода — все буферы вывода сбрасываются и очищаются
- Заголовки ответа — HTTP-код состояния и заголовки возвращаются к значениям по умолчанию
- Состояние ошибок — информация о последней ошибке (сообщение, файл, строка, тип) и статус соединения очищаются. Пользовательские обработчики ошибок (
set_error_handler()) и обработчики исключений (set_exception_handler()) сохраняются между запросами - Ini-директивы, изменённые запросом —
ini_set(),set_time_limit()иerror_reporting()откатываются к базовым значениям инициализации перед следующим запросом воркера. Границы описаны ниже
- Переменные во внешней области видимости — всё, что определено до
oxphp_worker()и захвачено черезuse - Статические свойства — статические свойства классов сохраняют свои значения
- Подключения к базе данных — PDO, MySQLi и другие постоянные соединения остаются открытыми
- Автозагрузчики — зарегистрированные автозагрузчики (Composer, собственные) остаются активными
- Загруженные классы и функции — все ранее загруженные классы, интерфейсы, трейты и функции
- Ini-директивы, заданные при инициализации —
ini_set()во внешней области видимости действует всё время жизни воркера, и именно к этим значениям откатываются изменения, сделанные в запросах
Откат ini-директив
Всё, что запрос изменил через ini_set(), set_time_limit() или error_reporting(), возвращается на место перед следующим запросом, который воркер берёт сам по себе, — как это было бы под PHP-FPM. ini_set('default_socket_timeout', 5) вокруг одного HTTP-вызова и set_time_limit(0) в фоновой ветке действуют для запроса, который их сделал, а не для следующего. Базовые значения, к которым они восстанавливаются, — это то, что задала ваша инициализация, а не значение из php.ini: ini_set() во внешней области видимости — это конфигурация приложения, и она переживает каждый запрос. Стоит знать три границы:
- Откат происходит, когда воркер берёт следующий запрос, не имея ничего другого в работе. Ini-директивы принадлежат потоку воркера, а не запросу. Воркер, обслуживающий несколько запросов сразу — а так бывает всякий раз, когда запрос приостанавливается на await, sleep или чтении сокета, а также пока ещё не прибран fire-and-forget-промис, — не может вернуть изменения одного запроса, не отняв их у другого, который ещё выполняется. Он и не пытается: пока у воркера есть работа в обработке, сделанные на нём изменения остаются видимыми запросам, которые он берёт в этом окне. Не полагайтесь на откат как на средство сдерживания директивы, утечка которой имела бы значение, — например
display_errors, — в приложении, обслуживающем запросы конкурентно. memory_limitвосстанавливается как значение раньше, чем как лимит. PHP отказывается опускать потолок аллокатора, пока занято больше нового лимита. Поэтому воркер, оставшийся с тем, что выделил запрос, сообщает черезini_get()восстановленныйmemory_limit, тогда как аллокатор всё ещё применяет повышенный. Потолок подтягивается, как только собственный объём памяти воркера оставляет для него место.opcache.enableне восстанавливается вовсе. Выключить OPcache — единственное, что запрос может с ним сделать (включить его обратно посреди запроса PHP отказывается), а снова поднимает его собственный по-запросный старт OPcache, который воркер выполняет один раз, при загрузке. Поэтому воркер, чей запрос выключил OPcache, до конца своей жизни компилирует каждый файл из исходников, и директива остаётся со значением0, чтобы об этом сообщать. Восстановление означало бы, чтоini_get('opcache.enable')иopcache_get_status()рапортуют о включённом кэше, который не работает, — вариант хуже, потому что приложению, которое спрашивает, чтобы что-то решить, сообщили бы противоположное тому, что происходит.
Перезапуск
Воркеры автоматически перезапускаются (перезапускаются с новым PHP-процессом) при выполнении любого из следующих условий:
- Превышен лимит памяти — использование памяти PHP воркером превышает
WORKER_MAX_MEMORY_MIBМиБ - Приложение запросило завершение — обработчик вызвал
Worker::scheduleExit(). Полезно для управляемой приложением горячей перезагрузки, перезагрузки на основе mtime файлов или повторного выполнения инициализации на каждый запрос - Последовательные ошибки — воркер взял 3 подряд запроса, которые развалились: фатальная ошибка, нехватка памяти, переполнение стека. После них остаётся состояние движка, которое унаследовал бы следующий запрос, — именно для этого перезапуск и нужен. Что считается, а что нет — см. ниже
Не каждый неудавшийся запрос считается, потому что не каждый сбой говорит о непригодности воркера к обслуживанию:
| Исход | Влияние на счётчик |
|---|---|
| Фатальная ошибка, нехватка памяти, переполнение стека — где бы они ни возникли, включая shutdown-функцию и деструктор, выполняемые в конце запроса | Считается |
Необработанное исключение (ответ 500) — из обработчика запроса или из shutdown-функции |
Нейтрально |
Отменённый запрос — клиент отключился, истёк max_execution_time, сервер завершает работу |
Нейтрально |
Запрос завершился, включая exit()/die() |
Обнуляет счётчик |
«Нейтрально» означает ровно это: такой исход посреди серии фаталов не добавляет к счётчику и не обнуляет его, так что fatal, exception, fatal, fatal всё равно перезапускает воркер. Где именно возник сбой, не влияет на то, как он трактуется. PHP выполняет shutdown-функции под собственной защитой, поэтому для воркера запрос, сломавшийся внутри одной из них, возвращается нормально, — но фатал там оставляет те же обломки, которые следующий запрос на этом воркере унаследовал бы от любого другого фатала, поэтому считается так же, а исключение оттуда разматывается так же чисто, как исключение из обработчика, поэтому так же нейтрально. Дедлайн — единственное, что читается по-разному в зависимости от того, когда он наступает: истечение во время работы shutdown-функции — это по-прежнему сервер, завершающий запрос, и оно остаётся нейтральным, если только запрос до этого уже не сломался сам — тогда он остаётся засчитанным. Чего всё это не делает — так это не диагностирует воркер, который завис, а не сломался: запрос, застрявший в системном вызове, попадает в oxphp_worker_stuck_total, чтобы оператор мог отреагировать, — он не отменяется и не считается.
Когда воркер перезапускается, PHP-процесс завершается и запускается новый, повторно выполняя внешнюю область видимости скрипта воркера. При перезапуске по памяти и по запланированному завершению текущий запрос обрабатывается штатно до конца, после чего воркер завершается. При перезапуске из-за ошибок воркер завершается после сбойного запроса.
Другие запросы, которые тот же воркер обслуживал конкурентно — приостановленные в oxphp_async_await(), oxphp_sleep() или на чтении сокета под RUNTIME_HOOKS, — закончить не успевают: каждый отменяется там, где приостановлен, и получает ответ 503 Service Unavailable с Retry-After, после выполнения собственных shutdown-функций. Перезапуск, таким образом, виден клиентам, чьи запросы оказались в обработке, — это стоит учитывать, выбирая WORKER_MAX_MEMORY_MIB или вызывая scheduleExit() на воркере, обслуживающем конкурентные запросы. Полное завершение работы сервера устроено иначе: там выполняющиеся запросы получают окно слива, чтобы завершиться нормально.
Перезагрузка при разработке
Режим воркеров хранит состояние инициализации (автозагрузчик, DI-контейнер, подключения к БД) в памяти, поэтому одного лишь opcache.validate_timestamps=1 недостаточно, чтобы подхватить изменения в коде, выполнявшемся во внешней области видимости. Для циклов разработки есть два варианта:
- Перезапуск на каждый запрос. Вызывайте
OxPHP\Server\Worker::current()->scheduleExit()в конце каждого вызова обработчика (например, ограничив это env-флагомOXPHP_DEV). Текущий запрос обрабатывается штатно до конца, затем воркер завершается и запускается заново, повторно выполняя внешнюю область видимости. Это меняет выигрыш в производительности от режима воркеров на семантику перезагрузки в стиле FPM. Это самый простой и надёжный подход для активной разработки. - Держите воркер «тёплым», перезагружайте обработчики запросов. Полностью откажитесь от
scheduleExit(), включитеopcache.validate_timestamps=1и держите инициализацию минимальной. Код, загруженный внутри колбэка запроса, будет обновлён OPcache при следующем запросе; код, загруженный один раз во внешней области видимости, — нет. Полный список нюансов см. в OPcache и JIT → Настройки для разработки.
Устранение неполадок
Запросы зависают и никогда не завершаются
Если oxphp_worker() никогда не вызывается в скрипте инициализации, ни один запрос не направляется, и каждый запрос ждёт бесконечно. Убедитесь, что ваш скрипт вызывает oxphp_worker() безусловно в нормальном пути выполнения кода.
Состояние протекает между запросами
Переменные, определённые внутри колбэка oxphp_worker(), очищаются сборщиком мусора PHP, но статические свойства и глобальные переменные, определённые во внешней области видимости, сохраняются. Если вы видите, что данные из одного запроса появляются в другом, проверьте статические свойства или глобальные переменные, накапливающие состояние между вызовами.
Решение: явно сбрасывайте статическое состояние в начале каждого колбэка запроса или не храните состояние отдельного запроса в статических свойствах.
Воркер немедленно перезапускается (лимит памяти)
Лимит памяти воркера проверяется после каждого запроса на основе данных PHP об использовании памяти. Если фаза инициализации выделяет большой объём памяти (например, загружает большой кеш), начальный объём занимаемой памяти может уже быть близок к лимиту.
Решение: увеличьте WORKER_MAX_MEMORY_MIB или отложите крупные выделения памяти до первого запроса.
Воркер немедленно перезапускается (лимит ошибок)
Три фатальные ошибки подряд вызывают перезапуск. Проверьте логи вашего приложения на предмет фаталов в колбэке запроса — но не необработанных исключений и не отменённых запросов: они не считаются.
Проверка: ищите ошибки в логе доступа или структурированном выводе логов:
docker logs <container> 2>&1 | grep '"level":"error"'Подключение к базе данных обрывается после простоя
Если ваш сервер базы данных закрывает простаивающие соединения, попытки переподключения в следующем запросе могут завершиться неудачей. Используйте пул соединений, который обрабатывает переподключение, или перехватывайте исключение и переподключайтесь вручную.
Пример для Docker
services:
app:
image: ghcr.io/oxphp/oxphp:0.11.0
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
environment:
- DOCUMENT_ROOT=/var/www/html/public
- WORKER_MODE_ENABLED=true
- ENTRY_FILE=/var/www/html/worker.php
- WORKER_MAX_MEMORY_MIB=128PHP API
Интроспекция воркера и точка входа воркера доступны через класс OxPHP\Server\Worker.
<?php
$worker = OxPHP\Server\Worker::current();
$worker->serve(function () {
handleRequest();
});Устаревшие свободные функции (oxphp_is_worker, oxphp_worker_id, oxphp_worker) по-прежнему доступны и работают через то же внутреннее состояние. В новом коде следует предпочитать API класса.
Класс также предоставляет интроспекцию во время выполнения, полезную для корректного самоперезапуска, наблюдаемости и проверок работоспособности:
| Метод | Возвращает |
|---|---|
Worker::isWorkerMode(): bool |
Работает ли сервер в режиме воркеров |
$worker->id(): int |
Стабильный идентификатор воркера в рамках потока |
$worker->startTime(): float |
Unix-метка времени запуска этого воркера |
$worker->requestCount(): int |
Количество запросов, обработанных этим воркером |
$worker->memoryUsage(): int |
Текущее значение memory_get_usage(true) для этого воркера |
$worker->rss(): int |
Текущий размер резидентной памяти в байтах (Linux/macOS) |
$worker->maxMemoryBytes(): int |
Порог перезапуска — WORKER_MAX_MEMORY_MIB × 1 МиБ, или 0 при отсутствии ограничений |
$worker->isExitScheduled(): bool |
Был ли вызван scheduleExit() |
$worker->exitReason(): ?string |
null во время работы; "scheduled", "max_memory" или "error", когда воркер завершается |
Полные сигнатуры и разобранные примеры см. в OxPHP\Server\Worker.
Примеры на PHP
Определение режима воркеров
Используйте OxPHP\Server\Worker::isWorkerMode(), чтобы проверить, работает ли текущий процесс в режиме воркеров. Это полезно для написания кода, который работает как в традиционном режиме, так и в режиме воркеров.
<?php
if (OxPHP\Server\Worker::isWorkerMode()) {
// Reuse a persistent connection
$redis = new Redis();
$redis->pconnect('redis', 6379);
} else {
// Traditional mode: connect per request
$redis = new Redis();
$redis->connect('redis', 6379);
}Скрипт воркера для Symfony
<?php
use App\Kernel;
require __DIR__ . '/../vendor/autoload.php';
$kernel = new Kernel('prod', false);
$kernel->boot();
oxphp_worker(function () use ($kernel) {
$request = Symfony\Component\HttpFoundation\Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
});
$kernel->shutdown();Рекомендации
- Задайте
WORKER_MAX_MEMORY_MIB(например,128), чтобы утекающий воркер перезапускался автоматически, а не поглощал ресурсы хоста. Дополните этоWorker::scheduleExit()для перезапуска, управляемого приложением. - Не храните состояние отдельного запроса в статических свойствах или глобальных переменных. Поскольку они сохраняются между запросами, оставшееся состояние от одного запроса может протечь в другой.
- Проверяйте мягкий сброс на раннем этапе. Добавьте
Worker::current()->scheduleExit()в ваш обработчик под флагом разработки и прогоните приложение целиком, от начала до конца. Это ловит ошибки протекания состояния до того, как вы перейдёте на долгоживущие воркеры. - Обрабатывайте таймауты простоя базы данных. Если ваш драйвер базы данных отключается после периода простоя, перехватывайте исключение и переподключайтесь или используйте пул соединений, который обрабатывает переподключение автоматически.
- Держите внешнюю область видимости минимальной. Инициализируйте только то, что действительно должно сохраняться: автозагрузчики, конфигурацию и разделяемые сервисы. Отложите настройку, специфичную для запроса, в колбэк.
См. также
- Маршрутизация — как режим воркеров встраивается в маршрутизацию URL
- Ранний ответ — отправить ответ немедленно и продолжить фоновую обработку
- Функции PHP — полный справочник по
oxphp_worker(),oxphp_is_worker()и другим встроенным функциям - Справочник по конфигурации — полный список переменных окружения