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

OxPHP використовує файбери PHP, щоб конкурентно обробляти кілька HTTP-запитів на одному робочому потоці. Коли запит викликає oxphp_sleep() або oxphp_async_await() (якщо асинхронний пул увімкнено), він призупиняється, а робочий потік одразу береться за наступний запит. Один воркер може обслуговувати сотні запитів в обробці без додаткових потоків. Є кілька контекстів-винятків; їх перелічено в розділі Точки призупинення.

Як це працює

Планувальник виконує багато запитів на одному потоці: кожен отримує власний файбер, і планувальник перемикається між ними щоразу, коли файбер призупиняється.

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

Вбудовані в 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

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

Запити, що не призупиняються, виконуються на повній швидкості без жодних накладних витрат на файбери:

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() або увімкніть 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
<?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.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
<?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.

Якщо режим воркерів уже ввімкнено, отже, виклик розташований там, де перемикання файберів заборонене або OxPHP не володіє контекстом, — такі місця перелічено в розділі Точки призупинення. Найчастіше туди потрапляють через файбер, який запустив сам застосунок (через new Fiber або через планувальник, яким він керує всередині запиту), і жодне налаштування цього не змінює.

Високе споживання памʼяті за великої кількості одночасних запитів

Кожен файбер працює на власному C-стеку, розмір якого задає ini-налаштування PHP fiber.stack_size (2 MiB на 64-бітних платформах, якщо налаштування не задано), плюс власний стек PHP VM. На межі в 256 файберів на робочий потік це становить 512 MiB C-стека на потік, але як адресний простір, відображений від операційної системи, а не як памʼять, що використовується: сторінка стека файбера стає резидентною лише тоді, коли до неї доходить код, що виконується на цьому файбері. Воркер зберігає кожен створений ним файбер разом зі стеками, доки не завершить роботу, і повторно використовує їх для наступних запитів, тож файбер, чий запит колись пішов у глибоку рекурсію, утримує ці сторінки, поки живе воркер.

Рішення: якщо жодному запиту не потрібен глибший C-стек, ніж обраний вами розмір, зменште fiber.stack_size у php.ini, щоб обмежити, скільки стека може утримувати будь-який окремий файбер:

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 — потрібні сторінки він прочитає сам.

prompt
Зроби мої обробники запитів безпечними для кооперативного планувальника 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