Мультиплексирование файберов
OxPHP использует файберы PHP для конкурентной обработки нескольких HTTP-запросов на одном рабочем потоке. Когда запрос вызывает oxphp_sleep() или oxphp_async_await() (если включён асинхронный пул), он приостанавливается, и рабочий поток немедленно берётся за следующий запрос. Один воркер может обслуживать сотни запросов в обработке без дополнительных потоков.
Как это работает
Планировщик выполняет множество запросов на одном потоке, выделяя каждому собственный файбер и переключаясь между ними каждый раз, когда файбер приостанавливается.
- Приходит запрос, и планировщик назначает его файберу — легковесному контексту выполнения с собственным стеком и состоянием PHP.
- Файбер выполняет обработчик
oxphp_worker(). Если обработчик завершается без приостановки, ответ отправляется, а файбер переиспользуется с нулевыми накладными расходами по сравнению с воркером, обрабатывающим один запрос. - Если обработчик вызывает приостанавливающую функцию (
oxphp_sleep(),oxphp_usleep(),oxphp_async_await()), файбер передаёт управление обратно планировщику. - Планировщик принимает новые входящие запросы (создавая новые файберы) и возобновляет приостановленные файберы, у которых выполнены условия ожидания (истёк таймер, готов асинхронный результат).
- Состояние 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. Не передаёт управление |
Встроенные в PHP sleep() и usleep() блокируют весь рабочий поток. Всегда используйте oxphp_sleep() и oxphp_usleep(), чтобы получить кооперативное поведение.
Примеры на 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())- Разрешение DNS (
gethostbyname()) - Любой синхронный сетевой или дисковый ввод-вывод
Как избежать блокировки
Оборачивайте блокирующие операции в 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.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
$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.
Высокое потребление памяти при большом числе конкурентных запросов
Каждый файбер использует C-стек (по умолчанию 8 MiB, настраивается ini-параметром PHP fiber.stack_size) плюс стек PHP VM на каждый запрос. При 256 конкурентных файберах в худшем случае память под C-стеки составляет 2 GiB на один рабочий поток.
Решение: уменьшите fiber.stack_size в 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(), если вам нужно, чтобы поток оставался кооперативным
Смотрите также
- Режим воркеров — постоянные PHP-процессы и API
oxphp_worker() - Асинхронные промисы — фоновый пул потоков для выгрузки блокирующего ввода-вывода
- SSE — потоковая передача в реальном времени в сочетании с кооперативной задержкой на файберах
- Функции PHP —
oxphp_sleep(),oxphp_usleep()и другие функции, работающие с файберами - Справочник по конфигурации —
WORKER_MODE_ENABLED,ENTRY_FILE,PHP_WORKERS,ASYNC_WORKERS