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 w jednym procesie:

  1. 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.
  2. 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() lub oxphp_async_await(), Fiber oddaje wątek, a worker przełącza się na kolejne żądanie.
  3. Pula asynchroniczna (opcjonalna). Osobne wątki systemowe dla zadań zgłaszanych przez oxphp_async(). Włączana przez ustawienie ASYNC_WORKERS > 0. Odizolowana od puli workerów, aby zadania w tle nie blokowały obsługi żądań HTTP.

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.

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.

bash
PHP_WORKERS=8 # exactly 8 workers PHP_WORKERS=0 # auto-detect (default): half of available CPU cores, minimum 1

Pula dynamiczna

Workery skalują się w górę i w dół w zależności od zapotrzebowania. Podaj minimalną i maksymalną liczbę oddzieloną dwukropkiem:

bash
PHP_WORKERS=2:16 # start with 2, scale up to 16 under load

Gdy 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.

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.

Gdy kolejka jest pełna (wszystkie workery są zajęte, a kolejka osiągnęła pojemność), OxPHP natychmiast zwraca klientowi odpowiedź 529 Site is Overloaded z nagłówkiem Retry-After. 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.

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 if full)"]
  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
  1. Terminacja TLS. Jeśli skonfigurowano TLS_CERT i TLS_KEY, OxPHP obsługuje TLS bezpośrednio. Nie jest potrzebny osobny reverse proxy.
  2. Parsowanie HTTP i ID żądania. Żądanie zostaje sparsowane, a unikalne ID żądania jest generowane (lub zachowywany jest przychodzący nagłówek X-Request-ID).
  3. 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ów Forwarded (RFC 7239) lub X-Forwarded-*. Rozpoznane IP jest używane we wszystkich kolejnych krokach, w tym w ograniczaniu liczby żądań i rejestrowaniu dostępu. Zobacz Zaufane proxy.
  4. 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.
  5. Rozpoznanie trasy. URL jest dopasowywany do skonfigurowanego trybu routingu (traditional, framework lub SPA). Wynikiem jest plik statyczny, skrypt PHP albo 404. Tryb worker, jeśli jest włączony, zmienia sposób, w jaki PHP wykonuje rozpoznany skrypt, ale nie zmienia samego rozpoznawania tras.
  6. 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-Modified i Cache-Control.
  7. Wykonanie PHP. Żądanie zostaje umieszczone w ograniczonej kolejce i pobrane przez dostępny worker. Jeśli kolejka jest pełna, klient natychmiast otrzymuje 529.
  8. Kompresja. Odpowiedzi tekstowe są kompresowane algorytmem Brotli przed wysłaniem, gdy klient przesyła Accept-Encoding: br (konfigurowalne przez COMPRESSION_LEVEL).
  9. Strumieniowanie SSE. Jeśli skrypt ustawia Content-Type: text/event-stream lub wywołuje oxphp_stream_flush(), OxPHP przełącza się w tryb strumieniowania: każde wywołanie flush() natychmiast wysyła fragment do klienta, bez buforowania całej odpowiedzi. W trybie worker SSE działa we współpracy z multipleksowaniem Fiberów.
  10. 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.
  11. 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:

Tryb standardowy (domyślny)

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.

Tryb worker

Utrzymuje procesy 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.

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
<?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. To sprawia, że /health nadaje się do sond liveness/readiness w Kubernetes.

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. Worker, który uległ awarii, jest automatycznie zastępowany nowym.
  • Automatyczne odradzanie workerów. OxPHP monitoruje kondycję wszystkich workerów PHP. Jeśli worker niespodziewanie zginie, na jego miejsce uruchamiany jest nowy worker, bez ręcznej interwencji.
  • Ochrona przez przeciwciśnienie. Ograniczona kolejka żądań zapobiega przeciążeniu. Gdy serwer osiągnie pełną pojemność, nowe żądania otrzymują 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 i czeka na zakończenie żądań w toku (do konfigurowalnego limitu czasu opróżniania) przed zakończeniem pracy.

Zobacz też