Przegląd architektury
OxPHP to serwer HTTP w postaci pojedynczego pliku binarnego, który zastępuje tradycyjny stos nginx + PHP-FPM. W jednym procesie obsługuje parsowanie HTTP, terminację TLS, routing, wykonywanie PHP, kompresję i observability, bez żadnych zewnętrznych zależności w czasie działania.
Jak działa OxPHP
OxPHP łączy dwie warstwy uruchomieniowe oraz opcjonalną pulę asynchroniczną w jednym procesie:
- Asynchroniczna warstwa HTTP. Sterowana zdarzeniami warstwa sieciowa przyjmuje połączenia TCP, przeprowadza uzgadnianie TLS (handshake), parsuje żądania HTTP i wysyła odpowiedzi. Obsługuje tysiące równoczesnych połączeń dzięki nieblokującemu I/O, więc jeden powolny klient nigdy nie blokuje pozostałych.
- Pula workerów PHP. Pula dedykowanych workerów PHP wykonuje Twoje skrypty PHP. W trybie standardowym każdy worker obsługuje jedno żądanie naraz. W trybie worker z włączonym multipleksowaniem Fiberów pojedynczy worker może obsługiwać wiele równoczesnych żądań: gdy skrypt wywołuje
oxphp_sleep()luboxphp_async_await(), Fiber oddaje wątek, a worker przełącza się na kolejne żądanie.
Obie warstwy komunikują się przez ograniczoną kolejkę. Gdy przychodzi żądanie HTTP wymagające wykonania PHP, warstwa asynchroniczna umieszcza je w kolejce. Dostępny worker PHP pobiera je, wykonuje skrypt i zwraca odpowiedź do warstwy asynchronicznej w celu dostarczenia jej klientowi.
Pula asynchroniczna jest opcjonalna. Wykonuje zadania zgłaszane przez oxphp_async() na osobnych wątkach systemowych, z własną kolejką zadań, i włącza się ją przez ustawienie ASYNC_WORKERS > 0. Jest odizolowana od puli workerów, więc zadania w tle nie blokują obsługi żądań HTTP.
Ten podział sprawia, że sieciowe I/O (przyjmowanie połączeń, odczyt nagłówków, kompresja odpowiedzi, serwowanie plików statycznych) nigdy nie rywalizuje o zasoby z wykonywaniem PHP. Każda warstwa skaluje się niezależnie.
Pula workerów
Pula workerów PHP określa, ile skryptów PHP może wykonywać się równocześnie. OxPHP obsługuje dwa tryby puli.
Pula statyczna
Stała liczba workerów uruchamia się przy starcie i działa przez cały czas życia serwera. To tryb domyślny.
PHP_WORKERS=8 # exactly 8 workers
PHP_WORKERS=0 # auto-detect (default): half of available CPU cores, minimum 1Pula dynamiczna
Workery skalują się w górę i w dół w zależności od zapotrzebowania. Podaj minimalną i maksymalną liczbę oddzieloną dwukropkiem:
PHP_WORKERS=2:16 # start with 2, scale up to 16 under loadGdy wszystkie bieżące workery są zajęte, OxPHP tworzy nowe workery aż do maksimum. Gdy worker pozostaje bezczynny dłużej niż PHP_WORKERS_IDLE_SECONDS (domyślnie: 30 sekund), zostaje wycofany z powrotem w stronę minimum.
Wycofanie następuje w momencie, gdy worker nie ma nic w toku, więc żądanie, które wciąż obsługuje, zawsze dobiega do własnej odpowiedzi. Ta sama reguła wyznacza granicę: worker trzymający żądanie, które nigdy się nie kończy (otwarty strumień, odczyt od strony, która przestała odpowiadać), nigdy nie osiąga takiego momentu i nie jest wycofywany, dopóki je trzyma.
Kolejka i przeciwciśnienie
Między asynchroniczną warstwą HTTP a pulą workerów znajduje się ograniczona kolejka. Jej pojemność domyślnie wynosi początkową liczbę workerów pomnożoną przez 128 i można ją nadpisać za pomocą QUEUE_CAPACITY. Dla puli statycznej liczbą początkową jest skonfigurowana liczba workerów. Dla puli dynamicznej (MIN:MAX) liczbą początkową jest wartość minimalna.
Żądanie, które trafia na pełną kolejkę, nie jest odrzucane od razu. Czeka na miejsce — do QUEUE_WAIT_TIMEOUT_MS (domyślnie: 1000 ms) — i zostaje przyjęte, gdy tylko worker pobierze żądanie znajdujące się przed nim. To ustawienie jest maksymalnym czasem oczekiwania, a nie stałą długością. Serwer skraca budżet, dopóki praca przyjęta z oczekiwania kończy się dla klientów, którzy już się rozłączyli, i przywraca go, gdy kolejka się opróżni, więc pod przeciążeniem termin wyznaczany nadchodzącemu żądaniu jest tym skróconym. Oczekujące żądania są przyjmowane w kolejności nadejścia. Budżet to pojedynczy termin wyznaczany w chwili nadejścia, który podąża z żądaniem do kolejki: gdy termin minie, żądanie otrzymuje odpowiedź 529, a worker, który pobierze żądanie po upływie jego terminu, odrzuca je, zamiast je wykonać. Jeśli klient zamknął do tego czasu połączenie, odpowiedzią jest zamiast tego 499, bo nie ma już klienta, który odebrałby odmowę, a liczenie tego jako 529 sprawiłoby, że seria przeciążeń mierzyłaby, jak długo klienci byli gotowi czekać. Budżet ogranicza więc całe oczekiwanie: przyjęcie i czas spędzony w kolejce. Ma to znaczenie, bo kolejka jest na tyle głęboka, że dotarcie do jej końca na wolnej puli trwa znacznie dłużej niż jakikolwiek budżet, jaki ustawiłby operator, oraz dlatego, że kolejka, z której żaden worker nie pobiera żądań, nie ma momentu pobrania, w którym dałoby się wyegzekwować termin. Zrzucanie obciążenia odbywa się na podstawie upłyniętego czasu oczekiwania, a nie głębokości kolejki w chwili nadejścia żądania: skok ruchu, który rozładowuje się w mikrosekundy, zostaje obsłużony, podczas gdy pula, która naprawdę nie nadąża, wciąż zrzuca.
Budżet ogranicza wyłącznie oczekiwanie, a nie wykonanie. Gdy worker pobierze żądanie przed jego terminem, budżet nie przerywa go, niezależnie od tego, jak długo potem działa handler, bo sprawdzenie dotyczy oczekiwania, a nie całkowitego upłyniętego czasu. Czas odpowiedzi pod obciążeniem to oczekiwanie plus czas wykonania, a budżet ogranicza tylko oczekiwanie. Ogranicza je za to w całości: żądanie, które wciąż czeka, gdy mija jego termin, dostaje odpowiedź w tym momencie — niezależnie od tego, czy czeka na miejsce w kolejce, czy stoi w kolejce na worker.
QUEUE_MAX_WAITING ogranicza, ile żądań może czekać jednocześnie (domyślnie: początkowa liczba workerów × 128, z pułapem połowy MAX_CONNECTIONS); powyżej tej wartości przyjmowanie wraca do natychmiastowego odrzucania. Oczekiwanie nie jest darmowe. Oczekujące żądanie trzyma swoje połączenie — oraz swoje już zbuforowane ciało — dopóki nie zostanie przyjęte albo budżet się nie wyczerpie, więc nieograniczony zbiór oczekujących zużyłby przy trwałym przeciążeniu wszystkie zezwolenia na połączenia, zablokował pętlę akceptującą i zostawił serwer zrywający nowe połączenia zamiast na nie odpowiadać. Wartość domyślna przybliża, ile żądań pula faktycznie zdoła przyjąć w ramach budżetu: czekanie ponad to jedynie odracza odrzucenie, trzymając w tym czasie te zasoby. Pułap MAX_CONNECTIONS / 2 ogranicza sam zbiór oczekujących, co samo w sobie nie stanowi zapasu dla pętli akceptującej: żądanie w kolejce trzyma swoje połączenie, dopóki worker do niego nie dotrze albo nie wyczerpie się jego budżet, wykonywane też je trzyma (jego miejsce w kolejce zostało zwolnione przy pobraniu), a QUEUE_CAPACITY nie ma żadnego pułapu wywiedzionego z liczby połączeń. Dlatego PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING musi pozostawać poniżej MAX_CONNECTIONS. Gdy suma osiągnie ten budżet, pętla akceptująca staje z wszystkimi zezwoleniami zajętymi, a klient, który wtedy nadejdzie, nie dostaje żadnej odpowiedzi zamiast 529. Serwer ostrzega o tym przy starcie, a samo zatrzymanie raportuje na bieżąco, gdy do niego dochodzi: WARN, gdy pętla staje, i INFO z czasem trwania, gdy wznawia pracę (z ograniczeniem częstotliwości, aby serwer, który raz po raz uderza w pułap, nie zalewał własnego logu), a do tego gauge oxphp_accept_stalled i licznik oxphp_accept_stalls_total. Oba są aktualizowane przy każdym zatrzymaniu, także przy tych, które ogranicznik logu pomija, i są serwowane przez wewnętrzny listener, poza tym budżetem; zobacz dokumentację metryk. Usunięcie ostrzeżenia startowego jest warunkiem koniecznym, lecz niewystarczającym: połączenia, które nigdy nie docierają do PHP, również trzymają zezwolenia, a sprawdzenie przy starcie nie jest w stanie przewidzieć zatrzymania, które one wywołają. Zwróć uwagę, że składniki tej sumy są liczone w połączeniach, podczas gdy budżet zużywają żądania: w HTTP/2 jedno połączenie niesie wiele żądań, więc wdrożenie o dużym udziale h2 osiąga limit przy ułamku swojego budżetu połączeń, powinno ustawić QUEUE_MAX_WAITING jawnie i może zasadnie znajdować się powyżej tej sumy.
Zbiór oczekujących jest ograniczony podwójnie, ponieważ limit liczony w żądaniach nic nie mówi o pamięci, którą te żądania zajmują: ta sama liczba oczekujących nie kosztuje nic przy GET-ach bez ciała, a gigabajty przy wysyłkach plików. QUEUE_MAX_WAITING_BYTES (domyślnie 64 MiB) ogranicza liczbę bajtów ciał żądań zaparkowanych jednocześnie. Powyżej tej wartości żądanie niosące ciało jest odrzucane natychmiast, zamiast zostać zaparkowane, podczas gdy żądania bez ciała czekają dalej normalnie. Dwóch rzeczy ten limit nie obejmuje — obie istniały przed budżetem oczekiwania: ciał już przekazanych do kolejki, które QUEUE_CAPACITY ogranicza w żądaniach, a nic nie ogranicza w bajtach, oraz pamięci, którą ciało zajmuje, gdy wciąż jest odczytywane z połączenia — tej nie ogranicza żaden limit zbiorczy.
OxPHP zwraca odpowiedź 529 Site is Overloaded z nagłówkiem Retry-After, gdy żądania nie da się obsłużyć, a nie jedynie opóźnić: budżet oczekiwania się wyczerpał albo jeden z powyższych limitów odmówił mu miejsca do czekania. Kod statusu 529 (niestandardowy, używany m.in. przez Cloudflare) wyraźnie odróżnia przeciążenie od błędów aplikacji (500) i prac serwisowych (503), dzięki czemu łatwiej skonfigurować alerty i load balancery. Ustawienie QUEUE_WAIT_TIMEOUT_MS=0 wyłącza oczekiwanie i odrzuca w chwili, gdy kolejka jest pełna.
Ponieważ oczekujące żądania zajmują połączenie, a nie miejsce w kolejce, pułap współbieżności pod przeciążeniem wyznacza MAX_CONNECTIONS, a nie QUEUE_CAPACITY. W HTTP/2 jest to MAX_CONNECTIONS pomnożone przez H2_MAX_CONCURRENT_STREAMS, bo każdy strumień niesie własne żądanie. Dobieraj budżet oczekiwania z tą świadomością: ciało żądania jest już zbuforowane, zanim żądanie dotrze do kolejki, więc dłuższy budżet oznacza, że naprawdę przeciążony serwer trzyma w pamięci proporcjonalnie więcej żądań — i ich ciał — zanim odpowie.
Przepływ żądań
Każde żądanie przechodzi przez ten sam potok, niezależnie od tego, czy serwuje plik statyczny, czy wykonuje PHP:
graph TD
Client(["Client"]) --> TLS["TLS termination<br/>(if configured)"]
TLS --> Parse["HTTP parsing + Request ID"]
Parse --> Proxy["Trusted proxy resolution<br/>(if TRUSTED_PROXIES set)"]
Proxy --> Rate["Rate limiting check"]
Rate --> Route{"Route resolution"}
Route -->|Static file| Cache["File cache / disk read"]
Cache --> Compress["Compression + Response headers"]
Route -->|PHP request| Queue["Bounded queue<br/>(529 past the wait budget)"]
Queue --> Worker["PHP worker executes script"]
Worker --> Normal["Normal response"]
Worker --> SSE["SSE streaming (chunked)"]
Worker --> Early["Early response (finish_request)<br/>+ background work"]
Compress --> Deliver(["Response to client"])
Normal --> Deliver
SSE --> Deliver
Early --> Deliver
- Terminacja TLS. Jeśli skonfigurowano
TLS_CERTiTLS_KEY, OxPHP obsługuje TLS bezpośrednio. Nie jest potrzebny osobny reverse proxy. - Parsowanie HTTP i ID żądania. Żądanie zostaje sparsowane, a unikalne ID żądania jest generowane (lub zachowywany jest przychodzący nagłówek
X-Request-ID). - Rozpoznanie zaufanych proxy. Jeśli ustawiono
TRUSTED_PROXIES, a łączące się IP jest zaufane, OxPHP wyodrębnia prawdziwe IP klienta, protokół i host z nagłówkówForwarded(RFC 7239) lubX-Forwarded-*. Przy słowie kluczowymcloudflareżądanie, którego łańcuch przekazywania pokazuje, że przyszło z węzła brzegowego Cloudflare, bierze zamiast tego IP klienta zCF-Connecting-IP. Rozpoznane IP jest używane we wszystkich kolejnych krokach, w tym w ograniczaniu liczby żądań i rejestrowaniu dostępu. Zobacz Zaufane proxy. - Ograniczanie liczby żądań. Jeśli ustawiono
RATE_LIMIT, IP klienta jest sprawdzane względem licznika żądań na dane IP. Żądania przekraczające limit natychmiast otrzymują odpowiedź429 Too Many Requests. - Rozpoznanie trasy. URL jest dopasowywany do skonfigurowanego trybu routingu (traditional, framework, SPA lub worker). Wynikiem jest plik statyczny, skrypt PHP albo 404. Tryb worker to osobny tryb routingu, który zastępuje pozostałe trzy: wymaga
ENTRY_FILEz rozszerzeniem.phpi kieruje do tego skryptu każde żądanie, które nie zostało obsłużone jako plik statyczny (zobacz Routing). - Pliki statyczne. Serwowane bezpośrednio z pamięci podręcznej w pamięci (dla często używanych plików) lub strumieniowane z dysku. OxPHP automatycznie dodaje nagłówki
ETag,Last-ModifiediCache-Control. - Wykonanie PHP. Żądanie zostaje umieszczone w ograniczonej kolejce i pobrane przez dostępny worker. Jeśli kolejka jest pełna, żądanie czeka na miejsce do
QUEUE_WAIT_TIMEOUT_MS(domyślnie: 1000 ms). Klient otrzymuje 529, gdy ten budżet się wyczerpie, albo od razu, gdy zbiór oczekujących osiągnął już jeden ze swoich limitów (zobacz Kolejka i przeciwciśnienie). - Kompresja. Odpowiedzi tekstowe są kompresowane przed wysłaniem tym z algorytmów Brotli, Zstandard i gzip, który nagłówek
Accept-Encodingklienta stawia najwyżej. Przy remisie pliki statyczne z pamięci podręcznej dostają Brotli, a wszystko inne Zstandard (konfigurowalne przezCOMPRESSION_ENCODINGSi zmienne poziomu dla poszczególnych kodowań). - Strumieniowanie SSE. Jeśli skrypt ustawia
Content-Type: text/event-streamlub wywołujeoxphp_stream_flush(), OxPHP przełącza się w tryb strumieniowania: każde wywołanieflush()natychmiast wysyła fragment do klienta, bez buforowania całej odpowiedzi. W trybie worker SSE działa we współpracy z multipleksowaniem Fiberów. - Wczesna odpowiedź. Wywołanie
oxphp_finish_request()natychmiast wysyła odpowiedź HTTP do klienta. Skrypt kontynuuje wykonywanie w tle (zapis logów, aktualizacja pamięci podręcznych, wysyłanie powiadomień), nie utrzymując otwartego połączenia. - Dostarczenie odpowiedzi. Ukończona odpowiedź jest odsyłana przez połączenie, a jeśli włączone jest rejestrowanie dostępu, zapisywany jest wpis w logu.
Tryb worker a tryb standardowy
OxPHP obsługuje dwa modele wykonywania PHP:
Tworzy świeże środowisko PHP dla każdego żądania. Autoloadery, konfiguracja i połączenia z bazą danych są inicjalizowane przy każdym żądaniu i zwalniane po jego zakończeniu. Ten model jest zgodny ze wszystkimi aplikacjami PHP od razu, bez dodatkowej konfiguracji.
Utrzymuje workery PHP przy życiu między żądaniami. Twoja aplikacja bootstrapuje się raz (ładowanie autoloadera, konfiguracji oraz nawiązanie połączeń z bazą danych), a następnie wchodzi w pętlę żądań. Między żądaniami OxPHP automatycznie resetuje superglobalne, bufory wyjścia i nagłówki odpowiedzi, zachowując stan ustanowiony podczas bootstrapu. Celowym wyjątkiem jest $_ENV: przy domyślnym w PHP auto_globals_jit=1 nie jest resetowane, więc stanowi stan workera, a nie stan żądania, i wartość zapisaną tam podczas jednego żądania odczytuje każde kolejne żądanie obsłużone przez tego workera. Zobacz Zmienne superglobalne.
Tryb worker eliminuje narzut uruchomieniowy przypadający na każde żądanie, co może znacząco skrócić czasy odpowiedzi w aplikacjach opartych na frameworkach (Laravel, Symfony itd.), gdzie bootstrap jest kosztowny.
Aby włączyć tryb worker, ustaw WORKER_MODE_ENABLED=true i skieruj ENTRY_FILE na skrypt PHP, który wywołuje oxphp_worker():
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new MyApp\Application();
oxphp_worker(function () use ($app) {
$app->handle();
});Szczegółowy przewodnik znajdziesz w Tryb worker.
Serwer wewnętrzny
Jeśli ustawiona jest zmienna INTERNAL_ADDR, OxPHP uruchamia osobny serwer HTTP na wskazanym porcie. Udostępnia trzy endpointy:
| Endpoint | Opis |
|---|---|
GET /health |
Stan zdrowia w formacie JSON (czas działania, liczniki żądań, połączenia, stan workerów). Zwraca 200 podczas normalnej pracy, 503 podczas degradacji. |
GET /metrics |
Metryki w formacie Prometheus — liczniki żądań, czas odpowiedzi, czas oczekiwania w kolejce, statystyki workerów, oszczędności z kompresji. |
GET /config |
Migawka aktywnej konfiguracji w formacie JSON. Ścieżki plików TLS są ukrywane. |
Serwer wewnętrzny nie przechodzi przez pulę workerów PHP ani przez ograniczoną kolejkę. Odpowiada bezpośrednio z asynchronicznej warstwy HTTP, dzięki czemu pozostaje dostępny nawet wtedy, gdy pula PHP jest w pełni obciążona. Dzięki temu nadaje się do sond Kubernetes — kierowanych na dedykowane endpointy (/health/liveness, /health/readiness i /health/startup), a nie na zbiorczy /health, który odpowiada 503 w stanach, których restart nie naprawia, i nie śledzi flagi zamykania ustawianej przez opróżniający się serwer. Zobacz Kontrole stanu.
Szczegóły znajdziesz w Serwer wewnętrzny.
Bezpieczeństwo
OxPHP zapewnia kilka gwarancji, dzięki którym Twoja aplikacja działa niezawodnie na produkcji:
- Izolacja żądań. Jeśli skrypt PHP ulega awarii lub wywołuje błąd krytyczny, dotyczy to tylko tego jednego żądania. Serwer nadal normalnie obsługuje wszystkie pozostałe żądania, a worker, który uległ awarii, zostaje przywrócony w sposób opisany poniżej.
- Automatyczne odradzanie workerów. OxPHP monitoruje kondycję wszystkich workerów PHP i zastępuje ten, który niespodziewanie zginie, bez ręcznej interwencji. Pula statyczna uzupełnia się do skonfigurowanej liczby, więc zastąpienie odbywa się jeden do jednego; pula dynamiczna uzupełnia się tylko do swojego minimum, a worker utracony powyżej niego jest odzyskiwany w ramach zwykłego skalowania w górę.
- Ochrona przez przeciwciśnienie. Ograniczona kolejka żądań zapobiega przeciążeniu. Gdy serwer osiągnie pełną pojemność, nowe żądanie czeka na miejsce tylko tak długo, jak pozwala budżet oczekiwania, a następnie otrzymuje odpowiedź 529 z nagłówkiem
Retry-After, zamiast czekać w kolejce w nieskończoność i powodować kaskadowe przekroczenia limitu czasu. - Ochrona przed path traversal. Wszystkie ścieżki URL są sanityzowane przed dostępem do systemu plików. Blokowane są zakodowane procentowo próby traversalu, segmenty
..oraz ścieżki wychodzące poza katalog główny dokumentów. - Łagodne zamknięcie. Po otrzymaniu SIGTERM lub SIGINT (Ctrl+C) OxPHP przestaje przyjmować nowe połączenia (gniazdo nasłuchujące pozostaje otwarte do zakończenia procesu; zobacz Łagodne zamknięcie) i czeka na zakończenie żądań w toku (do konfigurowalnego limitu czasu opróżniania) przed zakończeniem pracy.
Zobacz też
- Tryb worker — trwałe workery PHP oraz API
oxphp_worker() - Multipleksowanie Fiberów — setki równoczesnych żądań na pojedynczym workerze
- Strumieniowanie SSE — strumieniowanie zdarzeń z PHP
- Wczesna odpowiedź —
oxphp_finish_request()i przetwarzanie w tle - Asynchroniczne promisy —
oxphp_async()/oxphp_async_await() - Serwer wewnętrzny — health, metryki, konfiguracja
- Routing — trzy tryby routingu i sposób rozpoznawania URL-i
- Dokumentacja konfiguracji — pełna lista zmiennych środowiskowych
- Metryki — obserwowanie puli workerów i potoku żądań
- Szybki start — uruchom OxPHP w mniej niż 5 minut