Мультиплексирование файберов

OxPHP использует файберы PHP для конкурентной обработки нескольких HTTP-запросов на одном рабочем потоке. Когда запрос вызывает oxphp_sleep() или oxphp_async_await() (если включён асинхронный пул), он приостанавливается, и рабочий поток немедленно берётся за следующий запрос. Один воркер может обслуживать сотни запросов в обработке без дополнительных потоков.

Как это работает

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

  1. Приходит запрос, и планировщик назначает его файберу — легковесному контексту выполнения с собственным стеком и состоянием PHP.
  2. Файбер выполняет обработчик oxphp_worker(). Если обработчик завершается без приостановки, ответ отправляется, а файбер переиспользуется с нулевыми накладными расходами по сравнению с воркером, обрабатывающим один запрос.
  3. Если обработчик вызывает приостанавливающую функцию (oxphp_sleep(), oxphp_usleep(), oxphp_async_await()), файбер передаёт управление обратно планировщику.
  4. Планировщик принимает новые входящие запросы (создавая новые файберы) и возобновляет приостановленные файберы, у которых выполнены условия ожидания (истёк таймер, готов асинхронный результат).
  5. Состояние PHP каждого файбера (суперглобальные переменные, заголовки ответа, буферы вывода, стек 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["ответ (без приостановки, нулевые накладные расходы)"]

Конфигурация

Мультиплексирование файберов активируется автоматически при включённом режиме воркеров. Дополнительных переменных окружения не требуется.

Переменная Значение по умолчанию Описание
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_stream_flush() Немедленно отправляет фрагмент клиенту и возвращает управление. Используйте вместе с oxphp_sleep() в циклах SSE
oxphp_finish_request() Отправляет полный ответ и продолжает выполнение PHP. Не передаёт управление
Note

Встроенные в PHP sleep() и usleep() блокируют весь рабочий поток. Всегда используйте oxphp_sleep() и oxphp_usleep(), чтобы получить кооперативное поведение.

Примеры на PHP

Базовая конкурентная обработка

Запросы, которые не приостанавливаются, выполняются на полной скорости с нулевыми накладными расходами на файберы:

worker.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, не блокируя воркер:

worker.php
<?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():

worker.php
<?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())
  • Разрешение DNS (gethostbyname())
  • Любой синхронный сетевой или дисковый ввод-вывод

Как избежать блокировки

Оборачивайте блокирующие операции в oxphp_async(), чтобы выполнять их в асинхронном пуле потоков:

php
<?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
<?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);
Note

Соединения с базой данных нельзя передать в oxphp_async(), поскольку объекты не сериализуются между потоками. Создавайте соединение внутри асинхронного замыкания или выполняйте прямой запрос в файбере, если запрос достаточно быстрый, чтобы блокировка была приемлема.

Warning

oxphp_async() требует ASYNC_WORKERS > 0. Когда асинхронный пул отключён (значение по умолчанию), вызов oxphp_async() бросает OxPHP\Async\AsyncException.

Как переиспользуются файберы

C-стеки файберов выделяются один раз и переиспользуются между запросами. Когда файбер завершает обработку запроса, он не уничтожается: вместо этого он приостанавливается, возвращая управление планировщику, и добавляется в список свободных. Следующий запрос переиспользует существующий C-стек, что позволяет избежать затратного выделения памяти.

Стеки PHP VM (используемые для кадров вызовов функций) выделяются заново для каждого запроса и освобождаются, когда обработчик возвращает управление.

Пример для Docker

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.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
<?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 положительное значение:

bash
ASYNC_WORKERS=8
"Failed to dispatch async task" при использовании oxphp_async()

Асинхронный пул работает, но заполнен до предела.

Решение: увеличьте ASYNC_WORKERS или ASYNC_QUEUE_CAPACITY:

bash
ASYNC_WORKERS=8 ASYNC_QUEUE_CAPACITY=512
oxphp_sleep() не передаёт управление другим запросам

Мультиплексирование файберов работает только в режиме воркеров. В традиционном режиме oxphp_sleep() откатывается к блокирующему usleep().

Решение: включите режим воркеров, задав WORKER_MODE_ENABLED=true.

Высокое потребление памяти при большом числе конкурентных запросов

Каждый файбер использует C-стек (по умолчанию 8 MiB, настраивается ini-параметром PHP fiber.stack_size) плюс стек PHP VM на каждый запрос. При 256 конкурентных файберах в худшем случае память под C-стеки составляет 2 GiB на один рабочий поток.

Решение: уменьшите fiber.stack_size в php.ini, если ваше приложение не использует глубокую рекурсию:

php.ini
fiber.stack_size = 512K

Ограничения

  • Только режим воркеров — мультиплексирование файберов недоступно в традиционном режиме
  • 256 файберов на воркер — жёсткий лимит, не настраивается во время выполнения
  • Только кооперативность — код с интенсивной нагрузкой на CPU (плотные циклы, тяжёлые вычисления) лишает ресурсов другие файберы. Вытеснения нет
  • Блокирующий ввод-вывод блокирует поток — все блокирующие вызовы должны быть обёрнуты в oxphp_async() для настоящей конкурентности
  • Нативные sleep()/usleep() из PHP не знают о файберах — используйте oxphp_sleep()/oxphp_usleep()
  • oxphp_async_await_race() и oxphp_async_await_any() не передают управление — сейчас они блокируют даже внутри файбера. oxphp_async_await_all() действительно приостанавливает файбер на время ожидания, поэтому дружественна к файберам; для race/any используйте последовательные вызовы oxphp_async_await(), если вам нужно, чтобы поток оставался кооперативным

Смотрите также