Multipleksowanie fiberów
OxPHP wykorzystuje Fibery PHP do współbieżnej obsługi wielu żądań HTTP na jednym wątku workera. Gdy żądanie wywołuje oxphp_sleep() lub oxphp_async_await() (przy włączonej puli async), zostaje zawieszone, a wątek workera natychmiast przejmuje kolejne żądanie. Jeden worker może zarządzać setkami żądań w toku bez dodatkowych wątków. Istnieje kilka kontekstów, w których tak nie jest; wymieniono je w sekcji Punkty zawieszenia.
Jak to działa
Scheduler uruchamia wiele żądań na jednym wątku, przydzielając każdemu własny Fiber i przełączając się między nimi za każdym razem, gdy Fiber zostaje zawieszony.
- Nadchodzi żądanie, a scheduler przypisuje je do Fibera: lekkiego kontekstu wykonania z własnym stosem i stanem PHP.
- Fiber uruchamia handler
oxphp_worker(). Jeśli handler zakończy się bez zawieszenia, odpowiedź zostaje wysłana, a Fiber jest poddawany recyklingowi z zerowym narzutem w porównaniu z workerem obsługującym pojedyncze żądanie. - Jeśli handler wywoła funkcję zawieszającą (
oxphp_sleep(),oxphp_usleep(),oxphp_async_await()), Fiber oddaje sterowanie z powrotem do schedulera. - Scheduler przejmuje nowe napływające żądania (tworząc nowe Fibery) i wznawia zawieszone Fibery, których warunki oczekiwania zostały spełnione (upłynął timer, gotowy wynik async).
- Stan PHP każdego Fibera (
$_GET,$_POST,$_COOKIE,$_SERVER,$_FILES,$_REQUEST, nagłówki odpowiedzi, bufory wyjściowe, stos VM) jest zapisywany przy zawieszeniu i odtwarzany przy wznowieniu, dzięki czemu wznawiany Fiber zastaje własny stan, a nie to, co w międzyczasie działało na wątku.
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)"]
Wszystko, czego brakuje na tej liście, jest współdzielone przez żądania nakładające się na jednym wątku. Tym, co trzeba znać z nazwy, jest sesja: $_SESSION, identyfikator sesji i stojąca za nimi otwarta sesja istnieją w jednym egzemplarzu na wątek workera. Żądanie, które scheduler przyjmie obok zawieszonego żądania pozostawiającego otwartą sesję, dostaje tamtą sesję zamiast własnej, a session_write_close() ich nie rozdziela (zobacz Tryb worker → Stan sesji). $_ENV idzie dalej: z założenia jest stanem workera, więc to, co jedno żądanie tam zapisze, odczyta każde kolejne żądanie na tym workerze, niezależnie od tego, czy się nakładają (zobacz Superglobalne).
Konfiguracja
Multipleksowanie fiberów aktywuje się automatycznie po włączeniu trybu worker. Nie ma dodatkowych zmiennych środowiskowych.
| Zmienna | Wartość domyślna | Opis |
|---|---|---|
WORKER_MODE_ENABLED |
false |
Ustaw na true i wskaż ENTRY_FILE na plik startowy .php, aby włączyć tryb worker i multipleksowanie fiberów |
PHP_WORKERS |
CPU / 2 (min 1) | Liczba wątków workera. Każdy wątek uruchamia własny niezależny scheduler obsługujący do 256 współbieżnych fiberów |
Maksymalna liczba współbieżnych fiberów na wątek workera wynosi 256. Przy 4 wątkach workera OxPHP może obsłużyć jednocześnie do 1024 żądań w toku.
Punkty zawieszenia
Poniższe funkcje zawieszają bieżący Fiber i pozwalają innym żądaniom działać na tym samym wątku:
| Funkcja | Co się dzieje |
|---|---|
oxphp_sleep(float $seconds) |
Zawiesza Fiber na zadany czas. Pozostałe Fibery działają dalej, z wyjątkiem kontekstów wymienionych w uwadze poniżej, w których wywołanie czeka zamiast tego na wątku workera |
oxphp_usleep(int $microseconds) |
To samo co oxphp_sleep(), ale z dokładnością do mikrosekund (minimum 1 ms) |
oxphp_async_await(int $promise_id) |
Zawiesza Fiber do czasu zakończenia zadania async w tle na puli wątków. W tych samych kontekstach co oxphp_sleep() czeka zamiast tego na wątku workera |
Poniższe funkcje nie zawieszają Fibera:
| Funkcja | Zachowanie |
|---|---|
oxphp_stream_flush() |
Natychmiast wysyła fragment do klienta i zwraca sterowanie. Używaj razem z oxphp_sleep() w pętlach SSE |
oxphp_finish_request() |
Wysyła kompletną odpowiedź i kontynuuje wykonywanie PHP. Nie oddaje sterowania |
Wbudowane w PHP funkcje sleep() i usleep() domyślnie blokują cały wątek workera. Używaj oxphp_sleep() / oxphp_usleep() albo ustaw RUNTIME_HOOKS=sleep, aby natywne funkcje wbudowane również zawieszały fiber. Przydaje się to, gdy kod zewnętrzny wywołuje sleep() bezpośrednio. Zobacz Konfiguracja.
Zarówno funkcje oxphp_*, jak i przechwycone funkcje wbudowane zawieszają wykonanie tylko tam, gdzie przełączenie Fibera jest możliwe. Gdzie nie jest, czekają zamiast tego na wątku workera. Przechwycone funkcje wbudowane przekazują wywołanie natywnym sleep()/usleep(), więc samo oczekiwanie, a także to, co zwraca sleep(), gdy przerwie je sygnał, należą do funkcji wbudowanej. Dotyczy to handlera declare(ticks), obsługi sygnałów pcntl, budowania danych wejściowych żądania, FILTER_CALLBACK z filter_input_array() oraz destruktorów uruchamianych po rozpadzie żądania; lista nie jest zamknięta, a w pełnej postaci znajduje się w sekcji Hooki runtime.
Osobnym przypadkiem jest Fiber uruchomiony przez samą aplikację (przez new Fiber albo przez sterowany przez nią scheduler): OxPHP nie może wznowić kontekstu, którego nie jest właścicielem, więc uśpienie wewnątrz takiego Fibera zajmuje wątek workera. Tę część obciążenia wymiaruj tak, jakby hook był wyłączony.
Aby potwierdzić, że serwer przejął ustawienie, odczytaj runtime_hooks z oxphp_server_info() albo z /config serwera wewnętrznego. Nierozpoznana zmienna środowiskowa nigdzie nie jest błędem.
Przykłady w PHP
Podstawowa obsługa współbieżna
Żądania, które się nie zawieszają, działają z pełną prędkością i zerowym narzutem fiberów:
<?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";
});Nieblokujące wywołania API
Połącz oxphp_async() z oxphp_async_await(), aby wykonywać zewnętrzne wywołania API bez blokowania workera:
<?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 z kooperacyjnym uśpieniem
Przeplataj zdarzenia Server-Sent Events z innymi żądaniami przy użyciu oxphp_stream_flush() i 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)
}
});Blokujące operacje I/O
Multipleksowanie fiberów jest kooperacyjne, a nie wywłaszczające. Fiber, który wywoła funkcję blokującą, zamraża cały wątek workera: żaden inny Fiber na tym wątku nie może wykonać postępu.
Funkcje, które blokują workera
file_get_contents(),fopen(),fread()curl_exec(),curl_multi_exec()- Zapytania PDO,
mysqli_query() - Funkcje PHP
sleep(),usleep()(zamiast nich używajoxphp_sleep()albo włączRUNTIME_HOOKS=sleep, w granicach opisanych w sekcji Punkty zawieszenia) - Rozwiązywanie DNS (
gethostbyname()) - Wszelkie synchroniczne operacje I/O sieci lub dysku
Wyjątkiem — po włączeniu RUNTIME_HOOKS=streams — są odczyty z gniazd i stream_select(): oba zawieszają fiber zamiast wątku, co obejmuje klientów blokujących się na odczycie strumienia PHP (mysqlnd, phpredis, wrappery strumieni HTTP) oraz pętle czekające na kilka gniazd naraz. Zapisy, socket_select(), ext/curl, nawiązywanie połączeń i rozwiązywanie DNS nadal blokują, gdy włączone jest samo streams. Kategoria net zawiesza dodatkowo nawiązywanie połączeń (z wyjątkiem połączeń trwałych), uzgadnianie TLS i odczyty TLS, odczyty z gniazd unix://, udp:// i udg://, z par gniazd i ze strumieni wyeksportowanych z gniazd innych niż TCP, a także zapisy czekające na wolne miejsce, podczas gdy socket_select(), ext/curl i rozwiązywanie DNS nadal blokują. To, czy kategorie w ogóle zostały zainstalowane, raportują oxphp_server_info() i /config jako runtime_hooks. Zobacz Konfiguracja.
Jak uniknąć blokowania
Opakuj operacje blokujące w oxphp_async(), aby uruchomić je na puli wątków 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);W przypadku zapytań do bazy danych:
<?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);Połączeń z bazą danych nie można przekazać do oxphp_async(), ponieważ obiekty nie są serializowalne między wątkami. Utwórz połączenie wewnątrz domknięcia async albo wykonaj zapytanie bezpośrednio w Fiberze, jeśli jest ono na tyle szybkie, że blokowanie jest akceptowalne.
oxphp_async() wymaga ASYNC_WORKERS > 0. Gdy pula async jest wyłączona (domyślnie), wywołanie oxphp_async() rzuca OxPHP\Async\AsyncException.
Jak Fibery są poddawane recyklingowi
Stosy C fiberów są alokowane raz i wielokrotnie wykorzystywane pomiędzy żądaniami. Gdy Fiber zakończy obsługę żądania, nie jest niszczony; zamiast tego zawiesza się z powrotem do schedulera i trafia na listę wolnych. Kolejne żądanie ponownie wykorzystuje istniejący stos C, co pozwala uniknąć kosztownej alokacji pamięci.
Stosy VM PHP (używane dla ramek wywołań funkcji) są utrzymywane w ten sam sposób: stos każdego Fibera jest alokowany przy pierwszym uruchomieniu Fibera i wykorzystywany ponownie przez każde żądanie, które ten Fiber obsługuje. Żądanie, którego łańcuch wywołań go przerasta, dostaje kolejne strony, oddawane, gdy te wywołania się zakończą.
Przykład dla Dockera
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=8Przy tej konfiguracji każdy z 4 wątków workera może obsłużyć do 256 współbieżnych fiberów, a blokujące operacje I/O są przenoszone na 8 wątków workera async.
Rozwiązywanie problemów
Żądania stają się wolne, gdy jedno z nich wykonuje ciężkie operacje I/O
Fiber wywołuje funkcję blokującą (zapytanie do bazy danych, żądanie HTTP, odczyt pliku) bez oxphp_async(). Blokuje to cały wątek workera.
Rozwiązanie: Opakuj wywołania blokujące w 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."
Pula async nie jest skonfigurowana. Gdy ASYNC_WORKERS=0 (domyślnie), wszystkie funkcje async rzucają OxPHP\Async\AsyncException.
Rozwiązanie: Ustaw ASYNC_WORKERS na wartość dodatnią:
ASYNC_WORKERS=8"Failed to dispatch async task" podczas używania oxphp_async()
Pula async działa, ale jest w pełni obciążona.
Rozwiązanie: Zwiększ ASYNC_WORKERS lub ASYNC_QUEUE_CAPACITY:
ASYNC_WORKERS=8
ASYNC_QUEUE_CAPACITY=512oxphp_sleep() nie oddaje sterowania innym żądaniom
Multipleksowanie fiberów działa wyłącznie w trybie worker. W trybie tradycyjnym oxphp_sleep() przechodzi w blokujące usleep().
Rozwiązanie: Włącz tryb worker, ustawiając WORKER_MODE_ENABLED=true.
Jeśli tryb worker jest już włączony, wywołanie trafia w miejsce, gdzie przełączenie Fibera jest odrzucane albo OxPHP nie jest właścicielem kontekstu, zgodnie z listą w sekcji Punkty zawieszenia. Najczęściej dzieje się tak w Fiberze uruchomionym przez samą aplikację (przez new Fiber albo przez scheduler, którym steruje ona wewnątrz żądania), i żadne ustawienie tego nie zmienia.
Wysokie zużycie pamięci przy wielu współbieżnych żądaniach
Każdy Fiber działa na własnym stosie C, którego rozmiar określa ustawienie ini fiber.stack_size w PHP (2 MiB na platformach 64-bitowych, gdy ustawienie nie jest podane), oraz ma własny stos VM PHP. Przy limicie 256 fiberów na wątek workera daje to 512 MiB stosu C na wątek, ale jako przestrzeń adresową zmapowaną z systemu operacyjnego, a nie pamięć w użyciu: strona stosu Fibera staje się rezydentna dopiero wtedy, gdy dotrze do niej kod działający na tym Fiberze. Worker zachowuje każdy utworzony przez siebie Fiber, łącznie ze stosami, aż do swojego zakończenia i wykorzystuje je ponownie dla kolejnych żądań, więc Fiber, którego żądanie kiedyś zeszło w głęboką rekurencję, zatrzymuje te strony tak długo, jak żyje worker.
Rozwiązanie: Jeśli żadne żądanie nie potrzebuje głębszego stosu C niż rozmiar, który wybierzesz, zmniejsz fiber.stack_size w php.ini, aby ograniczyć, ile swojego stosu może zatrzymać pojedynczy Fiber:
fiber.stack_size = 512KZmniejsza to również przestrzeń adresową, do 128 MiB na wątek workera przy limicie fiberów. Pamięć rezydentna spada tylko tam, gdzie żądanie zeszłoby głębiej niż nowy rozmiar, a takie żądanie kończy się teraz niepowodzeniem: wywołanie, które zeszłoby głębiej, rzuca Error: Maximum call stack size of … bytes … reached.
Ograniczenia
- Tylko tryb worker — multipleksowanie fiberów nie jest dostępne w trybie tradycyjnym
- 256 fiberów na worker — twardy limit, niekonfigurowalny w czasie działania
- Tylko kooperacyjne — kod obciążający CPU (ciasne pętle, ciężkie obliczenia) zagładza pozostałe Fibery. Nie ma wywłaszczania
- Blokujące I/O blokuje wątek — wszystkie wywołania blokujące muszą być opakowane w
oxphp_async(), aby uzyskać prawdziwą współbieżność - Natywne funkcje PHP
sleep()/usleep()nie są świadome fiberów — używajoxphp_sleep()/oxphp_usleep()alboRUNTIME_HOOKS=sleep; żadne z nich nie zawiesza tam, gdzie przełączenie Fibera jest odrzucane lub kontekstem zarządza scheduler z przestrzeni użytkownika (zobacz Punkty zawieszenia) oxphp_async_await_race()ioxphp_async_await_any()nie oddają sterowania — obecnie blokują nawet wewnątrz Fibera.oxphp_async_await_all()zawiesza Fiber na czas oczekiwania, więc jest przyjazny fiberom; w przypadkurace/anyużywaj sekwencyjnych wywołańoxphp_async_await(), jeśli chcesz, aby wątek pozostał kooperacyjny
Prompty dla Twojego agenta
Te prompty zakładają agenta z podłączonym serwerem MCP dokumentacji OxPHP — sam przeczyta strony, których potrzebuje.
Spraw, by moje handlery żądań były bezpieczne dla kooperacyjnego schedulera OxPHP. Przeczytaj stronę `features/fiber-multiplexing` przez serwer MCP dokumentacji OxPHP.
Przejrzyj to repozytorium pod kątem wywołań, które blokują wątek workera zamiast zawiesić Fiber, i powiedz mi, które z nich przenieść na pulę asynchroniczną, które pokrywa już hook runtime, a które są na tyle krótkie, że można je zostawić.Zobacz także
- Tryb worker — trwałe workery PHP i API
oxphp_worker() - Async Promises — pula wątków w tle do przenoszenia blokujących operacji I/O
- SSE — strumieniowanie w czasie rzeczywistym w połączeniu z kooperacyjnym uśpieniem opartym na fiberach
- Funkcje PHP —
oxphp_sleep(),oxphp_usleep()i inne funkcje świadome fiberów - Dokumentacja konfiguracji —
WORKER_MODE_ENABLED,ENTRY_FILE,PHP_WORKERS,ASYNC_WORKERS