Мультиплексування файберів
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["Worker Thread"] W --> A0["Fiber A: handling /api/users"] A0 --> A1["oxphp_sleep(0.5)"] A1 --> A2["suspended"] A2 --> A3["resumed"] A3 --> A4["response"] W --> B0["Fiber B: handling /api/orders"] B0 --> B1["oxphp_async_await($p)"] B1 --> B2["suspended"] B2 --> B3["resumed"] B3 --> B4["response"] W --> C0["Fiber C: handling /health"] C0 --> C1["response (no suspension, zero overhead)"]
Усе, чого немає в цьому списку, спільне для запитів, які перетинаються в часі на одному потоці. Сесію варто знати поіменно: $_SESSION, ідентифікатор сесії та відкрита за ними сесія існують в одному екземплярі на робочий потік. Запит, який планувальник прийняв поруч із призупиненим запитом, що залишив сесію відкритою, отримує ту сесію, а не власну, і session_write_close() їх не розділяє (див. Режим воркерів → Стан сесії). З $_ENV усе ще суворіше: це стан воркера за задумом, тому те, що один запит туди записав, читають усі наступні запити на цьому воркері, перетинаються вони в часі чи ні (див. Суперглобальні змінні).
Налаштування
Мультиплексування файберів вмикається автоматично разом із режимом воркерів. Додаткових змінних середовища немає.
| Змінна | Типове значення | Опис |
|---|---|---|
WORKER_MODE_ENABLED |
false |
Встановіть true і вкажіть в ENTRY_FILE початковий .php-файл, щоб увімкнути режим воркерів і мультиплексування файберів |
PHP_WORKERS |
CPU / 2 (мін. 1) | Кількість робочих потоків. Кожен потік запускає власний незалежний планувальник із не більш ніж 256 одночасними файберами |
Максимальна кількість одночасних файберів на робочий потік — 256. З 4 робочими потоками OxPHP може одночасно обслуговувати до 1 024 запитів в обробці.
Точки призупинення
Ці функції призупиняють поточний файбер і дають іншим запитам виконуватися на тому самому потоці:
| Функція | Що відбувається |
|---|---|
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-рукостискання й читання з 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