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.
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 (superglobalne, nagłówki odpowiedzi, bufory wyjściowe, stos VM) jest zapisywany przy zawieszeniu i odtwarzany przy wznowieniu. Fibery są w pełni odizolowane od siebie.
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)"]
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 |
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 |
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() blokują cały wątek workera. Zawsze używaj oxphp_sleep() i oxphp_usleep(), aby uzyskać zachowanie kooperacyjne.
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()) - Rozwiązywanie DNS (
gethostbyname()) - Wszelkie synchroniczne operacje I/O sieci lub dysku
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ą alokowane od nowa dla każdego żądania i zwalniane, gdy handler zwróci sterowanie.
Przykład dla Dockera
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=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.
Wysokie zużycie pamięci przy wielu współbieżnych żądaniach
Każdy Fiber używa stosu C (domyślnie 8 MiB, konfigurowanego ustawieniem ini fiber.stack_size w PHP) oraz stosu VM PHP na każde żądanie. Przy 256 współbieżnych fiberach pesymistyczne zużycie pamięci stosów C wynosi 2 GiB na wątek workera.
Rozwiązanie: Zmniejsz fiber.stack_size w php.ini, jeśli Twoja aplikacja nie korzysta z głębokiej rekurencji:
fiber.stack_size = 512KOgraniczenia
- 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() 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
Zobacz także
- Tryb worker — trwałe procesy 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