Мультиплексирование файберов
OxPHP использует файберы PHP для конкурентной обработки нескольких HTTP-запросов на одном рабочем потоке. Когда запрос вызывает oxphp_sleep() или oxphp_async_await() (если включён асинхронный пул), он приостанавливается, и рабочий поток немедленно берётся за следующий запрос. Один воркер может обслуживать сотни запросов в обработке без дополнительных потоков. Есть несколько контекстов-исключений; они перечислены в разделе Точки приостановки.
Как это работает
Планировщик выполняет множество запросов на одном потоке, выделяя каждому собственный файбер и переключаясь между ними каждый раз, когда файбер приостанавливается.
- Приходит запрос, и планировщик назначает его файберу — легковесному контексту выполнения с собственным стеком и состоянием PHP.
- Файбер выполняет обработчик
oxphp_worker(). Если обработчик завершается без приостановки, ответ отправляется, а файбер переиспользуется с нулевыми накладными расходами по сравнению с воркером, обрабатывающим один запрос. - Если обработчик вызывает приостанавливающую функцию (
oxphp_sleep(),oxphp_usleep(),oxphp_async_await()), файбер передаёт управление обратно планировщику. - Планировщик принимает новые входящие запросы (создавая новые файберы) и возобновляет приостановленные файберы, у которых выполнены условия ожидания (истёк таймер, готов асинхронный результат).
- Состояние PHP каждого файбера (
$_GET,$_POST,$_COOKIE,$_SERVER,$_FILES,$_REQUEST, заголовки ответа, буферы вывода, стек VM) сохраняется при приостановке и восстанавливается при возобновлении, поэтому возобновлённый файбер находит своё собственное состояние, а не то, что оставил после себя выполнявшийся тем временем код.
graph LR W["Рабочий поток"] W --> A0["Файбер A: обработка /api/users"] A0 --> A1["oxphp_sleep(0.5)"] A1 --> A2["приостановлен"] A2 --> A3["возобновлён"] A3 --> A4["ответ"] W --> B0["Файбер B: обработка /api/orders"] B0 --> B1["oxphp_async_await($p)"] B1 --> B2["приостановлен"] B2 --> B3["возобновлён"] B3 --> B4["ответ"] W --> C0["Файбер C: обработка /health"] C0 --> C1["ответ (без приостановки, нулевые накладные расходы)"]
Всё, чего нет в этом списке, общее для запросов, которые пересекаются по времени на одном потоке. Сессию стоит знать поимённо: $_SESSION, идентификатор сессии и открытая за ними сессия существуют в одном экземпляре на рабочий поток. Запрос, который планировщик принял рядом с приостановленным запросом, оставившим сессию открытой, получает ту сессию, а не свою, и session_write_close() их не разделяет (см. Режим воркеров → Состояние сессии). С $_ENV всё ещё строже: это состояние воркера по замыслу, поэтому то, что один запрос туда записал, читают все последующие запросы на этом воркере, пересекаются они по времени или нет (см. Суперглобальные переменные).
Конфигурация
Мультиплексирование файберов активируется автоматически при включённом режиме воркеров. Дополнительных переменных окружения не требуется.
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
WORKER_MODE_ENABLED |
false |
Установите true и укажите в ENTRY_FILE файл-бутстрап .php, чтобы включить режим воркеров и мультиплексирование файберов |
PHP_WORKERS |
CPU / 2 (min 1) | Количество рабочих потоков. Каждый поток запускает собственный независимый планировщик с максимум 256 конкурентными файберами |
Максимальное количество конкурентных файберов на один рабочий поток — 256. При 4 рабочих потоках OxPHP может обрабатывать до 1024 запросов в обработке одновременно.
Точки приостановки
Эти функции приостанавливают текущий файбер и позволяют другим запросам выполняться на том же потоке:
| Функция | Что происходит |
|---|---|
oxphp_sleep(float $seconds) |
Приостанавливает файбер на заданное время. Остальные файберы продолжают выполняться — кроме контекстов, перечисленных в примечании ниже: там вызов ждёт на рабочем потоке |
oxphp_usleep(int $microseconds) |
То же, что и oxphp_sleep(), но с точностью до микросекунд (минимум 1 мс) |
oxphp_async_await(int $promise_id) |
Приостанавливает файбер до завершения асинхронной задачи в фоновом пуле потоков. В тех же контекстах, что и oxphp_sleep(), ждёт на рабочем потоке |
Эти функции не приостанавливают файбер:
| Функция | Поведение |
|---|---|
oxphp_stream_flush() |
Немедленно отправляет фрагмент клиенту и возвращает управление. Используйте вместе с oxphp_sleep() в циклах SSE |
oxphp_finish_request() |
Отправляет полный ответ и продолжает выполнение PHP. Не передаёт управление |
Встроенные в PHP sleep() и usleep() по умолчанию блокируют весь рабочий поток. Либо используйте oxphp_sleep() / oxphp_usleep(), либо задайте RUNTIME_HOOKS=sleep, чтобы нативные встроенные функции тоже приостанавливали файбер. Это полезно, когда сторонний код вызывает sleep() напрямую. См. Конфигурацию.
И функции oxphp_*, и перехваченные встроенные функции приостанавливают файбер только там, где возможно переключение файберов. Где оно невозможно, они ждут на рабочем потоке. Перехваченные встроенные функции передают вызов нативным sleep()/usleep(), поэтому и ожидание, и то, что sleep() возвращает, если его прерывает сигнал, — поведение встроенной функции. Сюда относятся обработчик declare(ticks), диспетчеризация сигналов pcntl, построение входных данных запроса, FILTER_CALLBACK из filter_input_array() и деструкторы, выполняемые после того, как запрос разобран; список открытый, полностью он приведён в разделе Хуки времени выполнения.
Отдельный случай — файбер, который приложение запускает само (через new Fiber или через планировщик, которым оно управляет): OxPHP не может возобновить контекст, которым не владеет, поэтому ожидание внутри такого файбера занимает рабочий поток. Рассчитывайте эту долю нагрузки так, будто хук выключен.
Чтобы убедиться, что сервер подхватил настройку, прочитайте runtime_hooks из oxphp_server_info() или из /config внутреннего сервера. Нераспознанная переменная окружения нигде не считается ошибкой.
Примеры на PHP
Базовая конкурентная обработка
Запросы, которые не приостанавливаются, выполняются на полной скорости с нулевыми накладными расходами на файберы:
<?php
oxphp_worker(function () {
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($path === '/health') {
echo json_encode(['status' => 'ok']);
return; // No suspension — runs at full speed
}
if ($path === '/slow') {
oxphp_sleep(2.0); // Yields for 2 seconds — other requests run
echo "Done after 2s delay";
return;
}
echo "Hello";
});Неблокирующие вызовы API
Комбинируйте oxphp_async() с oxphp_async_await(), чтобы выполнять внешние вызовы API, не блокируя воркер:
<?php
oxphp_worker(function () {
// Dispatch two API calls to the async thread pool
$p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users'));
$p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders'));
// Await both — the fiber suspends, other requests run on this thread
$users = oxphp_async_await($p1);
$orders = oxphp_async_await($p2);
header('Content-Type: application/json');
echo json_encode(['users' => json_decode($users), 'orders' => json_decode($orders)]);
});SSE с кооперативной задержкой
Чередуйте Server-Sent Events с другими запросами с помощью oxphp_stream_flush() и oxphp_sleep():
<?php
oxphp_worker(function () {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
for ($i = 0; $i < 30; $i++) {
echo "data: " . json_encode(['count' => $i, 'time' => time()]) . "\n\n";
oxphp_stream_flush(); // Send chunk now (does not suspend)
oxphp_sleep(1.0); // Yield for 1 second (other requests run)
}
});Блокирующий ввод-вывод
Мультиплексирование файберов кооперативное, а не вытесняющее. Файбер, вызывающий блокирующую функцию, замораживает весь рабочий поток: ни один другой файбер на этом потоке не может продвинуться.
Функции, блокирующие воркер
file_get_contents(),fopen(),fread()curl_exec(),curl_multi_exec()- Запросы через PDO,
mysqli_query() sleep(),usleep()из PHP (используйте вместо нихoxphp_sleep()или включитеRUNTIME_HOOKS=sleep— в пределах, описанных в разделе Точки приостановки)- Разрешение DNS (
gethostbyname()) - Любой синхронный сетевой или дисковый ввод-вывод
Чтение из сокетов и stream_select() становятся исключением, как только включён RUNTIME_HOOKS=streams: оба приостанавливают файбер вместо потока, что покрывает клиентов, блокирующихся на чтении PHP-потока (mysqlnd, phpredis, HTTP-обёртки потоков), а также циклы, ждущие сразу несколько сокетов. Записи, socket_select(), ext/curl, установка соединения и разрешение DNS при одной лишь streams по-прежнему блокируют. Категория net дополнительно приостанавливает файбер при установке соединения (кроме постоянных подключений), на TLS-рукопожатиях и чтениях, на чтениях из сокетов unix://, udp:// и udg://, из пар сокетов и из потоков, экспортированных из сокетов, отличных от TCP, а также на записях, ожидающих свободного места, тогда как socket_select(), ext/curl и разрешение DNS по-прежнему блокируют. Были ли категории вообще установлены, сообщает поле runtime_hooks в oxphp_server_info() и в /config. См. Конфигурацию.
Как избежать блокировки
Оборачивайте блокирующие операции в oxphp_async(), чтобы выполнять их в асинхронном пуле потоков:
<?php
// WRONG — blocks the entire worker thread
$html = file_get_contents('https://example.com');
// CORRECT — runs on async pool, fiber yields
$promise = oxphp_async(fn() => file_get_contents('https://example.com'));
$html = oxphp_async_await($promise);Для запросов к базе данных:
<?php
$db = new PDO('mysql:host=db;dbname=app', 'root', 'secret');
// WRONG — blocks the worker
$users = $db->query('SELECT * FROM users WHERE active = 1')->fetchAll();
// CORRECT — query runs on async thread, fiber yields
$promise = oxphp_async(function () {
$db = new PDO('mysql:host=db;dbname=app', 'root', 'secret');
return $db->query('SELECT * FROM users WHERE active = 1')->fetchAll();
});
$users = oxphp_async_await($promise);Соединения с базой данных нельзя передать в oxphp_async(), поскольку объекты не сериализуются между потоками. Создавайте соединение внутри асинхронного замыкания или выполняйте прямой запрос в файбере, если запрос достаточно быстрый, чтобы блокировка была приемлема.
oxphp_async() требует ASYNC_WORKERS > 0. Когда асинхронный пул отключён (значение по умолчанию), вызов oxphp_async() бросает OxPHP\Async\AsyncException.
Как переиспользуются файберы
C-стеки файберов выделяются один раз и переиспользуются между запросами. Когда файбер завершает обработку запроса, он не уничтожается: вместо этого он приостанавливается, возвращая управление планировщику, и добавляется в список свободных. Следующий запрос переиспользует существующий C-стек, что позволяет избежать затратного выделения памяти.
Стеки PHP VM (используемые для кадров вызовов функций) хранятся так же: стек каждого файбера выделяется при первом запуске файбера и переиспользуется каждым запросом, который этот файбер обслуживает. Запрос, чья цепочка вызовов перерастает его, получает дополнительные страницы, которые возвращаются, когда эти вызовы завершаются.
Пример для Docker
services:
app:
image: ghcr.io/oxphp/oxphp:0.13.0
ports:
- "80:80"
environment:
- DOCUMENT_ROOT=/var/www/html/public
- WORKER_MODE_ENABLED=true
- ENTRY_FILE=worker.php
- PHP_WORKERS=4
- ASYNC_WORKERS=8При такой конфигурации каждый из 4 рабочих потоков может обрабатывать до 256 конкурентных файберов, а блокирующий ввод-вывод выгружается на 8 асинхронных рабочих потоков.
Устранение неполадок
Запросы становятся медленными, когда один запрос выполняет тяжёлый ввод-вывод
Файбер вызывает блокирующую функцию (запрос к базе данных, HTTP-запрос, чтение файла) без oxphp_async(). Это блокирует весь рабочий поток.
Решение: оберните блокирующие вызовы в oxphp_async():
<?php
$promise = oxphp_async(fn() => file_get_contents($url));
$result = oxphp_async_await($promise);"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."
Асинхронный пул не настроен. Когда ASYNC_WORKERS=0 (значение по умолчанию), все асинхронные функции бросают OxPHP\Async\AsyncException.
Решение: задайте ASYNC_WORKERS положительное значение:
ASYNC_WORKERS=8"Failed to dispatch async task" при использовании oxphp_async()
Асинхронный пул работает, но заполнен до предела.
Решение: увеличьте ASYNC_WORKERS или ASYNC_QUEUE_CAPACITY:
ASYNC_WORKERS=8
ASYNC_QUEUE_CAPACITY=512oxphp_sleep() не передаёт управление другим запросам
Мультиплексирование файберов работает только в режиме воркеров. В традиционном режиме oxphp_sleep() откатывается к блокирующему usleep().
Решение: включите режим воркеров, задав WORKER_MODE_ENABLED=true.
Если режим воркеров уже включён, значит, вызов находится там, где переключение файберов запрещено или OxPHP не владеет контекстом, — такие места перечислены в разделе Точки приостановки. Чаще всего туда попадают через файбер, запущенный самим приложением (через new Fiber или через планировщик, которым оно управляет внутри запроса), и никакая настройка этого не меняет.
Высокое потребление памяти при большом числе конкурентных запросов
Каждый файбер работает на собственном C-стеке, размер которого задаёт ini-параметр PHP fiber.stack_size (2 MiB на 64-битных платформах, если параметр не задан), плюс собственный стек PHP VM. При пределе в 256 файберов на рабочий поток это даёт 512 MiB C-стека на поток, но как адресное пространство, отображённое операционной системой, а не как используемая память: страница стека файбера становится резидентной, только когда до неё доходит код, выполняющийся на этом файбере. Воркер хранит каждый созданный им файбер вместе со стеками до своего завершения и переиспользует их для последующих запросов, поэтому файбер, запрос которого однажды ушёл в глубокую рекурсию, держит эти страницы всё время жизни воркера.
Решение: если ни одному запросу не нужен C-стек глубже выбранного вами размера, уменьшите fiber.stack_size в php.ini, чтобы ограничить, сколько своего стека может удерживать любой отдельный файбер:
fiber.stack_size = 512KЭто также сокращает адресное пространство — до 128 MiB на рабочий поток при пределе файберов. Резидентная память уменьшается только там, где запрос ушёл бы глубже нового размера, и такой запрос теперь вместо этого завершается ошибкой: вызов, который ушёл бы глубже, выбрасывает Error: Maximum call stack size of … bytes … reached.
Ограничения
- Только режим воркеров — мультиплексирование файберов недоступно в традиционном режиме
- 256 файберов на воркер — жёсткий лимит, не настраивается во время выполнения
- Только кооперативность — код с интенсивной нагрузкой на CPU (плотные циклы, тяжёлые вычисления) лишает ресурсов другие файберы. Вытеснения нет
- Блокирующий ввод-вывод блокирует поток — все блокирующие вызовы должны быть обёрнуты в
oxphp_async()для настоящей конкурентности - Нативные
sleep()/usleep()из PHP не знают о файберах — используйтеoxphp_sleep()/oxphp_usleep()илиRUNTIME_HOOKS=sleep; ни то, ни другое не приостанавливает файбер там, где переключение файберов запрещено или контекстом владеет пользовательский планировщик (см. Точки приостановки) oxphp_async_await_race()иoxphp_async_await_any()не передают управление — сейчас они блокируют даже внутри файбера.oxphp_async_await_all()действительно приостанавливает файбер на время ожидания, поэтому дружественна к файберам; дляrace/anyиспользуйте последовательные вызовыoxphp_async_await(), если вам нужно, чтобы поток оставался кооперативным
Промпты для вашего агента
Эти промпты рассчитаны на агента с подключённым MCP-сервером документации OxPHP — нужные страницы он прочитает сам.
Сделай мои обработчики запросов безопасными для кооперативного планировщика OxPHP. Прочитай страницу `features/fiber-multiplexing` через MCP-сервер документации OxPHP.
Пройдись по этому репозиторию в поисках вызовов, которые блокируют поток воркера вместо того, чтобы приостановить файбер, и скажи мне, какие из них перенести в асинхронный пул, какие уже закрыты хуком времени выполнения, а какие достаточно коротки, чтобы их не трогать.Смотрите также
- Режим воркеров — постоянные PHP-воркеры и API
oxphp_worker() - Асинхронные промисы — фоновый пул потоков для выгрузки блокирующего ввода-вывода
- SSE — потоковая передача в реальном времени в сочетании с кооперативной задержкой на файберах
- Функции PHP —
oxphp_sleep(),oxphp_usleep()и другие функции, работающие с файберами - Справочник по конфигурации —
WORKER_MODE_ENABLED,ENTRY_FILE,PHP_WORKERS,ASYNC_WORKERS