Tryb worker
Tryb worker uruchamia trwałe procesy PHP, które inicjalizują się raz, a następnie obsługują wiele żądań, dzięki czemu koszt uruchomienia PHP jest ponoszony tylko jeden raz, a nie przy każdym żądaniu. Zamiast burzyć i odbudowywać stan PHP przy każdym żądaniu, aplikacja ładuje swój autoloader, konfigurację i połączenia z bazą danych jeden raz i wykorzystuje je ponownie przez cały czas życia workera.
Jak to działa
- Włącz tryb worker. Ustaw
WORKER_MODE_ENABLED=truei skierujENTRY_FILEna swój skrypt startowy. Włącza to tryb worker dla wszystkich workerów PHP w puli. - Inicjalizacja raz. PHP uruchamia się i wykonuje zakres zewnętrzny jeden raz. Rejestracja autoloadera, wczytanie konfiguracji, połączenia z bazą danych oraz pozostały kod inicjalizacyjny wykonują się tylko jeden raz.
- Wejście w pętlę żądań. Wywołaj
oxphp_worker(callback). OxPHP zaczyna przekazywać przychodzące żądania HTTP do Twojego callbacka. - Reset między żądaniami. Superglobalne (
$_GET,$_POST,$_SERVER,$_COOKIE,$_FILES,$_REQUEST,php://input), bufory wyjścia, nagłówki odpowiedzi oraz dyrektywy ini zmienione przez żądanie są resetowane automatycznie — co to obejmuje i gdzie się kończy, opisuje Co jest resetowane, a co zachowywane. Miękki reset czyści stan poszczególnych żądań, zachowując zainicjalizowane zasoby w zakresie zewnętrznym. Wyjątkiem jest$_ENV, które celowo nie jest resetowane — zobacz Superglobalne. - Zakres zewnętrzny jest zachowywany. Zmienne zdefiniowane przed
oxphp_worker(), właściwości statyczne, połączenia z bazą danych i autoloadery pozostają dostępne we wszystkich żądaniach obsługiwanych przez danego workera.
Tryb worker zmienia zachowanie routingu. Wszystkie żądania, które nie pasują do pliku statycznego na dysku, są przekazywane do workera zamiast zwracać 404. Szczegóły znajdziesz w Routing.
Konfiguracja
| Zmienna | Domyślnie | Opis |
|---|---|---|
WORKER_MODE_ENABLED |
false |
Włącza trwały tryb worker. Akceptuje true, 1, yes. Wymaga, aby ENTRY_FILE wskazywał na skrypt .php |
ENTRY_FILE |
(nieustawione) | Ścieżka do skryptu startowego workera. Rozwiązywana względem DOCUMENT_ROOT, gdy jest względna; segmenty .. i ścieżki bezwzględne są dozwolone (skrypty startowe workera znajdujące się poza publicznym katalogiem dokumentów to obsługiwany układ) |
WORKER_MAX_MEMORY_MIB |
0 |
Maksymalna pamięć PHP na workera w MiB przed recyklingiem. 0 = bez limitu |
Starsza zmienna WORKER_FILE jest nadal parsowana (z ostrzeżeniem WARN przy starcie) i zachowuje się jak WORKER_MODE_ENABLED=true ENTRY_FILE=$WORKER_FILE. Nowe wdrożenia powinny używać jawnej pary; starsza forma zostanie usunięta w przyszłej wersji.
W przypadku recyklingu sterowanego przez aplikację wywołaj OxPHP\Server\Worker::scheduleExit() wewnątrz procedury obsługi żądania. Worker kończy działanie czysto po zakończeniu bieżącego żądania.
Pisanie skryptu workera
Skrypt workera składa się z dwóch części: zakresu zewnętrznego, który wykonuje się raz przy starcie, oraz callbacka przekazywanego do oxphp_worker(), który wykonuje się przy każdym żądaniu.
<?php
// Outer scope: runs once at startup
require __DIR__ . '/../vendor/autoload.php';
$config = parse_ini_file(__DIR__ . '/../config/app.ini');
$db = new PDO($config['dsn'], $config['user'], $config['pass'], [
PDO::ATTR_PERSISTENT => true,
]);
$app = new MyApp\Application($config, $db);
// Request loop: runs for every request
oxphp_worker(function () use ($app) {
$app->handle();
});
// Shutdown: runs when the worker exits
$app->terminate();Co jest resetowane, a co zachowywane
OxPHP wykonuje miękki reset między żądaniami. Stan poszczególnych żądań jest czyszczony automatycznie, natomiast wszystko, co zostało zainicjalizowane w zakresie zewnętrznym, przetrwa przez cały czas życia workera.
- Superglobalne —
$_GET,$_POST,$_SERVER,$_COOKIE,$_FILESiphp://inputsą ponownie wypełniane danymi nowego żądania - Bufory wyjścia — wszystkie bufory wyjścia są opróżniane i czyszczone
- Nagłówki odpowiedzi — kod statusu HTTP i nagłówki są resetowane do wartości domyślnych
- Stan błędów — informacje o ostatnim błędzie (komunikat, plik, wiersz, typ) oraz status połączenia są czyszczone. Zarejestrowane przez użytkownika procedury obsługi błędów (
set_error_handler()) i procedury obsługi wyjątków (set_exception_handler()) są zachowywane między żądaniami - Dyrektywy ini zmienione przez żądanie —
ini_set(),set_time_limit()ierror_reporting()wracają do stanu bazowego z inicjalizacji przed kolejnym żądaniem workera. Granice opisano poniżej
- Zmienne w zakresie zewnętrznym — wszystko, co zdefiniowano przed
oxphp_worker()i przechwycono przezuse - Właściwości statyczne — statyczne właściwości klas zachowują swoje wartości
- Połączenia z bazą danych — PDO, MySQLi i inne trwałe połączenia pozostają otwarte
- Autoloadery — zarejestrowane autoloadery (Composer, własne) pozostają aktywne
- Załadowane klasy i funkcje — wszystkie wcześniej załadowane klasy, interfejsy, traity i funkcje
- Dyrektywy ini ustawione podczas inicjalizacji —
ini_set()w zakresie zewnętrznym obowiązuje przez cały czas życia workera i to do niego wracają zmiany wykonane w żądaniach
Wycofywanie zmian ini
Wszystko, co żądanie zmieniło przez ini_set(), set_time_limit() lub error_reporting(), zostaje przywrócone, zanim worker weźmie swoje następne żądanie samodzielnie — tak, jak byłoby pod PHP-FPM. ini_set('default_socket_timeout', 5) wokół jednego wywołania HTTP i set_time_limit(0) w gałęzi tła dotyczą żądania, które je wykonało, a nie następnego. Stanem bazowym, do którego są przywracane, jest to, co ustawiła Twoja inicjalizacja, a nie wartość z php.ini: ini_set() w zakresie zewnętrznym to konfiguracja aplikacji i przeżywa każde żądanie. Warto znać trzy granice:
- Wycofanie następuje, gdy worker bierze swoje następne żądanie, nie mając nic innego w toku. Dyrektywy ini należą do wątku workera, nie do żądania. Worker obsługujący kilka żądań naraz — a tak jest zawsze, gdy żądanie zatrzymuje się na await, uśpieniu lub odczycie z gniazda, a także dopóki obietnica typu fire-and-forget nie zostanie jeszcze odebrana — nie może przywrócić zmian jednego żądania, nie odbierając ich innemu, które wciąż działa. Nie próbuje tego robić: dopóki worker ma pracę w toku, zmiany na nim wykonane pozostają widoczne dla żądań, które w tym oknie przyjmuje. Nie polegaj na wycofaniu, by powstrzymać dyrektywę, której wyciek miałby znaczenie — na przykład
display_errors— w aplikacji obsługującej żądania współbieżnie. memory_limitjest przywracany jako wartość, zanim zostanie przywrócony jako limit. PHP odmawia obniżenia pułapu alokatora, dopóki w pamięci trzymane jest więcej niż nowy limit. Worker, któremu zostało to, co zaalokowało żądanie, raportuje więc przezini_get()przywróconymemory_limit, podczas gdy alokator wciąż egzekwuje ten podniesiony. Pułap dogania wartość, gdy tylko własny ślad pamięciowy workera zostawi na to miejsce.opcache.enablenie jest przywracany wcale. Wyłączenie OPcache to jedyne, co żądanie może z nim zrobić (PHP odmawia włączenia go z powrotem w trakcie żądania), a tym, co go ponownie podnosi, jest własny start OPcache wykonywany raz na żądanie — który worker uruchamia raz, przy starcie. Worker, którego żądanie wyłączyło OPcache, kompiluje więc każdy plik ze źródła do końca swojego życia, a dyrektywa pozostaje z wartością0, żeby to sygnalizować. Przywrócenie jej sprawiłoby, żeini_get('opcache.enable')iopcache_get_status()raportowałyby włączony cache, który nie działa — opcja gorsza, bo aplikacja pytająca po to, by coś rozstrzygnąć, dostałaby odpowiedź przeciwną do tego, co się dzieje.
Recykling
Workery są automatycznie recyklowane (restartowane ze świeżym procesem PHP), gdy spełniony jest którykolwiek z poniższych warunków:
- Przekroczenie maksymalnej pamięci — zużycie pamięci PHP przez workera przekracza
WORKER_MAX_MEMORY_MIBMiB - Żądanie zakończenia przez aplikację — procedura obsługi wywołała
Worker::scheduleExit(). Przydatne przy sterowanym przez aplikację hot reload, przeładowaniu na podstawie mtime plików lub ponownym wykonaniu inicjalizacji dla każdego żądania - Kolejne błędy — worker wziął 3 kolejne żądania, które się rozpadły: błąd krytyczny, brak pamięci, przepełnienie stosu. To, co po nich zostaje, to stan silnika, który odziedziczyłoby następne żądanie — i właśnie po to jest recykling. Poniżej opisano, co się liczy, a co nie
Nie każde nieudane żądanie się liczy, bo nie każde niepowodzenie mówi, że worker nie nadaje się do obsługi:
| Wynik | Wpływ na licznik |
|---|---|
| Błąd krytyczny, brak pamięci, przepełnienie stosu — niezależnie od tego, gdzie zostały zgłoszone, wliczając funkcję zamykającą (shutdown function) i destruktor uruchamiane na końcu żądania | Liczy się |
Nieprzechwycony wyjątek (odpowiedź 500) — z procedury obsługi żądania lub z funkcji zamykającej |
Neutralny |
Anulowane żądanie — klient się rozłączył, upłynął max_execution_time, serwer się zamyka |
Neutralny |
Żądanie ukończone, wliczając exit()/die() |
Zeruje licznik |
„Neutralny" znaczy dokładnie to: takie zdarzenie w środku serii błędów krytycznych ani nie zwiększa licznika, ani go nie zeruje, więc fatal, wyjątek, fatal, fatal nadal recykluje workera. Miejsce zgłoszenia niepowodzenia nie ma znaczenia dla jego interpretacji. PHP uruchamia funkcje zamykające pod własną ochroną, więc worker widzi, że żądanie, które zawiodło w ich środku, kończy się normalnie — ale błąd krytyczny zostawia tam te same zgliszcza, które następne żądanie na tym workerze odziedziczyłoby po każdym innym błędzie krytycznym, więc liczy się tak samo, a wyjątek odwija się tam równie czysto jak ten z procedury obsługi, więc jest neutralny w ten sam sposób. Terminem, który jako jedyny jest odczytywany różnie w zależności od momentu, jest deadline: jego upłynięcie w trakcie działania funkcji zamykającej to nadal serwer kończący żądanie i pozostaje neutralne — chyba że żądanie już wcześniej zawiodło samo z siebie, wtedy pozostaje policzone. Czego to wszystko nie robi, to diagnozowanie workera, który się zaciął, a nie zawiódł: żądanie uwięzione w syscallu jest raportowane przez oxphp_worker_stuck_total, żeby zareagował operator — nie jest anulowane i nie jest liczone.
Gdy worker jest recyklowany, proces PHP kończy działanie i uruchamia się nowy, ponownie wykonując zakres zewnętrzny skryptu workera. W przypadku zakończenia opartego na pamięci oraz zaplanowanego zakończenia bieżące żądanie kończy się normalnie, zanim worker zakończy działanie. W przypadku recyklingu opartego na błędach worker kończy działanie po nieudanym żądaniu.
Inne żądania, które ten sam worker obsługiwał współbieżnie — zawieszone w oxphp_async_await(), oxphp_sleep() lub odczycie z gniazda pod RUNTIME_HOOKS — nie dostają szansy na dokończenie: każde jest anulowane tam, gdzie jest zawieszone, i po wykonaniu własnych funkcji zamykających otrzymuje odpowiedź 503 Service Unavailable z nagłówkiem Retry-After. Recykling jest więc widoczny dla klientów, których żądania akurat są w toku — warto o tym pamiętać przy doborze WORKER_MAX_MEMORY_MIB lub wywoływaniu scheduleExit() na workerze obsługującym żądania współbieżne. Pełne zamknięcie serwera jest inne: tam żądania w toku dostają okno wygaszania, aby zakończyć się normalnie.
Przeładowanie w trybie deweloperskim
Tryb worker przechowuje stan inicjalizacji (autoloader, kontener DI, połączenia z bazą danych) w pamięci, więc samo opcache.validate_timestamps=1 nie wystarczy, aby wychwycić zmiany w kodzie, który wykonał się w zakresie zewnętrznym. W pętlach deweloperskich są dwie opcje:
- Recyklinguj każde żądanie. Wywołuj
OxPHP\Server\Worker::current()->scheduleExit()na końcu każdego wywołania procedury obsługi (na przykład warunkowo, w zależności od zmiennej środowiskowejOXPHP_DEV). Bieżące żądanie kończy się normalnie, po czym worker kończy działanie i jest uruchamiany ponownie, wykonując zakres zewnętrzny na nowo. To zamienia zysk wydajnościowy trybu worker na semantykę przeładowania w stylu FPM. Jest to najprostsze i najbardziej niezawodne podejście przy aktywnej pracy deweloperskiej. - Utrzymuj workera rozgrzanego, przeładowuj procedury obsługi żądań. Całkowicie pomiń
scheduleExit(), włączopcache.validate_timestamps=1i utrzymuj minimalną inicjalizację. Kod załadowany wewnątrz callbacka żądania zostanie odświeżony przez OPcache przy następnym żądaniu; kod załadowany raz w zakresie zewnętrznym — nie. Pełną listę zastrzeżeń znajdziesz w OPcache i JIT → Ustawienia deweloperskie.
Rozwiązywanie problemów
Żądania zawieszają się i nigdy się nie kończą
Jeśli oxphp_worker() nigdy nie zostanie wywołane w skrypcie startowym, żadne żądania nie są przekazywane i każde żądanie czeka w nieskończoność. Upewnij się, że Twój skrypt wywołuje oxphp_worker() bezwarunkowo w normalnej ścieżce kodu.
Stan przecieka między żądaniami
Zmienne zdefiniowane wewnątrz callbacka oxphp_worker() są sprzątane przez garbage collector PHP, ale właściwości statyczne i zmienne globalne zdefiniowane w zakresie zewnętrznym są zachowywane. Jeśli widzisz dane z jednego żądania pojawiające się w innym, sprawdź, czy nie ma właściwości statycznych lub zmiennych globalnych, które kumulują stan między wywołaniami.
Rozwiązanie: Resetuj stan statyczny jawnie na początku każdego callbacka żądania lub unikaj przechowywania stanu poszczególnych żądań w polach statycznych.
Worker natychmiast się recykluje (limit pamięci)
Limit pamięci workera jest sprawdzany po każdym żądaniu na podstawie zgłaszanego przez PHP zużycia pamięci. Jeśli faza inicjalizacji alokuje dużą ilość pamięci (np. ładowanie dużego cache'u), początkowy ślad pamięciowy może już być bliski limitu.
Rozwiązanie: Zwiększ WORKER_MAX_MEMORY_MIB lub odłóż duże alokacje do pierwszego żądania.
Worker natychmiast się recykluje (limit błędów)
Trzy kolejne błędy krytyczne wyzwalają recykling. Sprawdź logi aplikacji pod kątem błędów krytycznych w callbacku żądania — nie nieprzechwyconych wyjątków ani anulowanych żądań, które się nie liczą.
Sprawdź: Poszukaj błędów w logu dostępu lub w ustrukturyzowanym wyjściu logów:
docker logs <container> 2>&1 | grep '"level":"error"'Połączenie z bazą danych zrywa się po okresie bezczynności
Jeśli Twój serwer bazy danych zamyka bezczynne połączenia, próby ponownego połączenia w kolejnym żądaniu mogą się nie powieść. Użyj puli połączeń, która obsługuje ponowne łączenie, lub przechwyć wyjątek i połącz się ponownie ręcznie.
Przykład dla Dockera
services:
app:
image: ghcr.io/oxphp/oxphp:0.11.0
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
environment:
- DOCUMENT_ROOT=/var/www/html/public
- WORKER_MODE_ENABLED=true
- ENTRY_FILE=/var/www/html/worker.php
- WORKER_MAX_MEMORY_MIB=128API PHP
Introspekcja workera oraz punkt wejścia workera są udostępniane przez klasę OxPHP\Server\Worker.
<?php
$worker = OxPHP\Server\Worker::current();
$worker->serve(function () {
handleRequest();
});Starsze funkcje globalne (oxphp_is_worker, oxphp_worker_id, oxphp_worker) są nadal dostępne i korzystają z tego samego stanu wewnętrznego. Nowy kod powinien preferować API klasy.
Klasa udostępnia również introspekcję w czasie działania przydatną przy łagodnym samodzielnym recyklingu, obserwowalności i kontrolach stanu:
| Metoda | Zwraca |
|---|---|
Worker::isWorkerMode(): bool |
Czy serwer działa w trybie worker |
$worker->id(): int |
Stabilne ID workera dla danego wątku |
$worker->startTime(): float |
Uniksowy znacznik czasu uruchomienia tego workera |
$worker->requestCount(): int |
Liczba żądań obsłużonych przez tego workera |
$worker->memoryUsage(): int |
Bieżące memory_get_usage(true) dla tego workera |
$worker->rss(): int |
Bieżący resident set size w bajtach (Linux/macOS) |
$worker->maxMemoryBytes(): int |
Próg recyklingu — WORKER_MAX_MEMORY_MIB × 1 MiB lub 0, gdy bez limitu |
$worker->isExitScheduled(): bool |
Czy scheduleExit() zostało wywołane |
$worker->exitReason(): ?string |
null w trakcie działania; "scheduled", "max_memory" lub "error", gdy worker jest wyłączany |
Pełne sygnatury i przykłady z rozwiązaniami znajdziesz w OxPHP\Server\Worker.
Przykłady PHP
Wykrywanie trybu worker
Użyj OxPHP\Server\Worker::isWorkerMode(), aby sprawdzić, czy bieżący proces działa w trybie worker. Jest to przydatne przy pisaniu kodu, który działa zarówno w trybie tradycyjnym, jak i w trybie worker.
<?php
if (OxPHP\Server\Worker::isWorkerMode()) {
// Reuse a persistent connection
$redis = new Redis();
$redis->pconnect('redis', 6379);
} else {
// Traditional mode: connect per request
$redis = new Redis();
$redis->connect('redis', 6379);
}Skrypt workera dla Symfony
<?php
use App\Kernel;
require __DIR__ . '/../vendor/autoload.php';
$kernel = new Kernel('prod', false);
$kernel->boot();
oxphp_worker(function () use ($kernel) {
$request = Symfony\Component\HttpFoundation\Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
});
$kernel->shutdown();Dobre praktyki
- Ustaw
WORKER_MAX_MEMORY_MIB(np.128), aby przeciekający worker recyklował się automatycznie zamiast pochłaniać zasoby hosta. Połącz to zWorker::scheduleExit(), aby dodatkowo uzyskać recykling sterowany przez aplikację. - Unikaj przechowywania stanu poszczególnych żądań we właściwościach statycznych lub zmiennych globalnych. Ponieważ są one zachowywane między żądaniami, pozostały stan z jednego żądania może przeciec do innego.
- Wcześnie zweryfikuj miękki reset. Dodaj
Worker::current()->scheduleExit()do swojej procedury obsługi pod flagą deweloperską i przetestuj aplikację end-to-end. Pozwala to wychwycić błędy przeciekania stanu, zanim postawisz na długo żyjące workery. - Obsłuż limity czasu bezczynności bazy danych. Jeśli sterownik bazy danych rozłącza się po okresie bezczynności, przechwyć wyjątek i połącz się ponownie lub użyj puli połączeń, która automatycznie obsługuje ponowne łączenie.
- Utrzymuj minimalny zakres zewnętrzny. Inicjalizuj tylko to, co naprawdę musi być zachowane: autoloadery, konfigurację i współdzielone usługi. Konfigurację specyficzną dla żądania odłóż do callbacka.
Zobacz też
- Routing — jak tryb worker włącza się w routing adresów URL
- Wczesna odpowiedź — wyślij odpowiedź natychmiast i kontynuuj przetwarzanie w tle
- Funkcje PHP — pełna dokumentacja
oxphp_worker(),oxphp_is_worker()i innych funkcji wbudowanych - Dokumentacja konfiguracji — pełna lista zmiennych środowiskowych