Dokumentacja konfiguracji

OxPHP konfiguruje się w całości za pomocą zmiennych środowiskowych. Nie ma żadnych plików konfiguracyjnych do zarządzania, a każde ustawienie ma wartość domyślną, dzięki czemu wdrożenie bez żadnej konfiguracji działa od razu po uruchomieniu.

Wartości logiczne

Zmienne oznaczone jako logiczne akceptują ustalony, kanoniczny zbiór wartości — niewrażliwy na wielkość liter i pozbawiony spacji brzegowych:

  • prawda: on, true, 1, yes
  • fałsz: off, false, 0, no

Każda niepusta wartość spoza tego zbioru — literówki w rodzaju ture — powoduje natychmiastowy błąd przy starcie, wskazujący nazwę zmiennej. Dzięki temu błędna konfiguracja zostaje wychwycona jeszcze przed ruchem, zamiast po cichu ustawić flagę na odwrót.

Nieustawiona zmienna lub puste przypisanie (FOO=) wraca do udokumentowanej wartości domyślnej. Puste jest celowo traktowane jak nieustawione: podstawianie w Docker Compose / Kubernetes w rodzaju FOO=${FOO} daje FOO=, gdy zmienna hosta nie istnieje, a to nie powinno uniemożliwiać uruchomienia serwera.

Serwer

Zmienna Wartość domyślna Opis
LISTEN_ADDR 0.0.0.0:80 Adres i port głównego serwera HTTP
DOCUMENT_ROOT /var/www/html/public Katalog główny do serwowania plików i skryptów PHP
ENTRY_FILE (nieustawione) Pojedynczy kanoniczny skrypt wejściowy. Nieustawione = bezpośrednie mapowanie plików. *.php = front controller. Inny niż .php = statyczny fallback (SPA). Z WORKER_MODE_ENABLED=true = bootstrap workera. Rozwiązywany względem DOCUMENT_ROOT (ścieżki względne i .. są dozwolone; ścieżki absolutne używane bez zmian). Zobacz Routing
WORKER_MODE_ENABLED false Włącza trwały tryb worker. Wymaga, aby ENTRY_FILE wskazywał na skrypt .php. Wartość logiczna — zobacz Wartości logiczne
MAX_CONNECTIONS 10000 Maksymalna liczba jednoczesnych połączeń TCP. Także pułap dla domyślnego QUEUE_MAX_WAITING (jego połowa), więc nieprawidłowa wartość to błąd startu, a nie ciche przejście na wartość domyślną. Obniżenie jej nie przesuwa domyślnego QUEUE_CAPACITY, które jest wymiarowane wyłącznie z liczby workerów — utrzymuj ją powyżej PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING, zobacz Zachowanie zapasu dla pętli akceptującej
TOKIO_WORKERS CPU / 2 (min 1) Wątki asynchronicznego I/O. 1 = jednowątkowo, N > 1 = stała liczba wątków, nieustawione = automatycznie (CPU / 2, min 1)

Workery PHP

Zmienna Wartość domyślna Opis
EXECUTOR sapi Backend wykonawczy PHP. sapi do wykonywania PHP, stub do benchmarków bez PHP
PHP_WORKERS CPU / 2 (min 1) Rozmiar puli workerów. N = stała pula, MIN:MAX = skalowanie dynamiczne, 0 = automatycznie
PHP_WORKERS_IDLE_SECONDS 30 Liczba sekund bezczynności workera dynamicznego, po których zostaje wycofany (tylko tryb dynamiczny). Worker jest wycofywany w momencie, gdy nie ma nic w toku, więc nic, co obsługuje, nie zostaje ucięte — a worker trzymający żądanie, które nigdy się nie kończy, na przykład otwarty strumień, nigdy nie osiąga takiego momentu i nie jest wycofywany wcale
QUEUE_CAPACITY Początkowa liczba workerów × 128 Maksymalna liczba oczekujących żądań w kolejce PHP. Żądania trafiające na pełną kolejkę czekają na miejsce (zobacz QUEUE_WAIT_TIMEOUT_MS). W przypadku pul dynamicznych (MIN:MAX) początkowa liczba workerów = wartość minimalna. 0 = automatycznie
QUEUE_WAIT_TIMEOUT_MS 1000 Jak długo żądanie może czekać na workera PHP, zanim zostanie odrzucone z 529. Jeden termin, wyznaczany w chwili nadejścia, obejmujący oba oczekiwania, jakie mogą spotkać żądanie: na miejsce w kolejce oraz wewnątrz kolejki na workera. Żądanie, do którego worker dotrze po upływie terminu, jest przy pobraniu odrzucane, a nie wykonywane, więc budżet ogranicza całe oczekiwanie, nie tylko jego połowę związaną z przyjęciem — nie ogranicza natomiast tego, jak długo potem działa handler. Jednocześnie czeka co najwyżej QUEUE_MAX_WAITING żądań; powyżej tego żądania są odrzucane natychmiast. 0 = odrzucaj natychmiast, gdy tylko kolejka jest pełna. Obniż go lub ustaw 0, gdy aplikacja wywołuje przez HTTP ten sam serwer (wewnętrzne wywołanie nie może zostać przyjęte, dopóki zewnętrzne nie zwolni swojego workera, więc oczekiwanie jest zmarnowane) albo gdy load balancer z przodu ma własny, krótszy limit czasu. Klient, który zamknie połączenie w trakcie oczekiwania, natychmiast oddaje swoje miejsce — na HTTP/1.1 tak samo jak na HTTP/2 — więc balancer, który przekracza limit i zamyka, nie zapełnia zbioru oczekujących próbami, które już porzucił. Czego serwer nie widzi, to klient, który przestaje czekać bez zamykania — trzyma on swoje miejsce, aż zostanie przyjęty albo budżet się wyczerpie, a jeśli worker zwolni się pierwszy, jego skrypt działa dla nikogo — więc utrzymywanie budżetu poniżej limitu czasu wszystkiego, co stoi z przodu, nadal się opłaca
QUEUE_MAX_WAITING Początkowa liczba workerów × 128, z pułapem MAX_CONNECTIONS / 2 Maksymalna liczba żądań zaparkowanych jednocześnie w oczekiwaniu na miejsce w kolejce. Powyżej niej żądania są odrzucane natychmiast, zamiast czekać. Każdy oczekujący trzyma połączenie i w pełni zbuforowane ciało żądania przez cały czas oczekiwania, więc to ograniczenie trzymanych zasobów, a nie oczekiwań, które się opłacą. Pułap MAX_CONNECTIONS / 2 na wartości domyślnej ogranicza tylko tę część zaległości; żądania w kolejce i wykonywane trzymają połączenie tak samo, więc zapas, jaki serwer faktycznie zachowuje na przyjmowanie i odmawianie, wynika z PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING względem MAX_CONNECTIONS — zobacz niżej. Wymiaruj ją na podstawie opóźnienia własnego handlera — również niżej. 0 = automatycznie, nigdy poniżej 1
QUEUE_MAX_WAITING_BYTES 67108864 (64 MiB) Maksymalna łączna liczba bajtów ciał żądań, jaką mogą wspólnie trzymać zaparkowane żądania. Żądanie, którego ciało przekroczyłoby ten limit, jest odrzucane z 529 natychmiast, zamiast czekać; żądanie bez ciała nigdy nie jest z tego powodu odrzucane. QUEUE_MAX_WAITING ogranicza ten sam zbiór w żądaniach, co nic nie mówi o ich rozmiarze — ciała są buforowane w całości, zanim żądanie dotrze do kolejki, więc z samym licznikiem jako ogranicznikiem zaparkowany zbiór może trzymać QUEUE_MAX_WAITING × 10 MiB. Podnieś go dla aplikacji z dużą liczbą wysyłek plików, która ma amortyzować skoki dużych ciał zamiast je zrzucać; obniż na kontenerze z twardym limitem pamięci. 0 = automatycznie

Wymiarowanie zbioru oczekujących

Ile żądań może czekać z pożytkiem, wynika z tego, jak szybko pula je zdejmuje. Przy W workerach i handlerze zajmującym T milisekund pula przyjmuje W / T żądań na milisekundę, więc budżet B milisekund może wpuścić około W × B / T oczekujących. Wszystko ponad to czeka cały budżet i i tak zostaje odrzucone, trzymając przez cały ten czas połączenie i zbuforowane ciało żądania.

Wartość domyślna nie może tego policzyć — tempo obsługi nie jest znane przy starcie — więc jest celowo hojna. To pasuje do szybkich handlerów, gdzie pula rozładowuje głęboką zaległość dobrze w granicach budżetu, a jest o wiele za duża dla wolnych: przy 8 workerach, domyślnym budżecie 1 s i handlerze 200 ms na czas przyjąć da się tylko około 40 żądań, podczas gdy domyślnie parkowanych jest do 1024.

bash
QUEUE_MAX_WAITING=40

Ustawienie jej w pobliżu W × B / T zamienia nadwyżkę w natychmiastowe 529 zamiast 529 sekundę później. Skrócenie QUEUE_WAIT_TIMEOUT_MS osiąga to samo drugim składnikiem. Oba wymieniają się nawzajem, a przy wolnym handlerze mniejszy budżet jest zwykle lepszą dźwignią, bo ogranicza też, jak długo klient czeka na odmowę.

Zbiór jest ograniczony po raz drugi, w bajtach. Każdy oczekujący trzyma swoje ciało żądania w pamięci przez cały czas oczekiwania, więc limit liczony w żądaniach zostawia trzymaną przez nie pamięć ruchowi: te same 1024 oczekujących nie kosztują nic przy GET-ach bez ciała, a gigabajty przy wysyłkach. QUEUE_MAX_WAITING_BYTES ogranicza tę sumę wprost — powyżej niej żądanie niosące ciało jest odrzucane od razu, zamiast zostać zaparkowane, podczas gdy żądania bez ciała czekają dalej normalnie. Aplikacja z dużą liczbą wysyłek, która ma amortyzować skoki zamiast je zrzucać, chce większej wartości; kontener z twardym limitem pamięci — mniejszej. Odmowy dla każdego z limitów są liczone osobno w oxphp_admission_refused_total (waiting_full dla licznika, waiting_bytes dla pamięci), więc metryka wskazuje, po którą gałkę sięgnąć.

Zachowanie zapasu dla pętli akceptującej

Żądanie trzyma swoje połączenie — i jedno z zezwoleń MAX_CONNECTIONS — od chwili nadejścia aż do udzielenia odpowiedzi, cokolwiek robi w międzyczasie. Obejmuje to trzy osobne populacje, bo miejsce w kolejce jest zwalniane w momencie, gdy worker pobiera żądanie, przed uruchomieniem skryptu:

  • wykonywane w workerze: co najmniej PHP_WORKERS, a w trybie worker więcej, bo jeden wątek multipleksuje fibery;
  • w kolejce, do QUEUE_CAPACITY;
  • zaparkowane przy przyjmowaniu, do QUEUE_MAX_WAITING.

Tylko trzecia ma pułap wywiedziony z MAX_CONNECTIONS. Utrzymuj PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING poniżej MAX_CONNECTIONS. Powyżej tej granicy tryb awarii zmienia się na gorszy: pętla akceptująca bierze zezwolenie na połączenie, zanim zacznie je obsługiwać, więc gdy ścieżka PHP trzyma je wszystkie, pętla staje, a klient, który wtedy nadejdzie, nie dostaje żadnej odpowiedzi zamiast 529 — czego load balancer nie odróżni od martwej instancji, podczas gdy sonda stanu na INTERNAL_ADDR pozostaje zielona, bo nie przechodzi przez żaden z tych limitów.

Serwer sprawdza to przy starcie i ostrzega, gdy suma osiąga budżet, wymieniając każdą wartość; oxphp config --check raportuje to samo, nie zmieniając swojego werdyktu ani kodu wyjścia. Traktuj usunięcie ostrzeżenia jako warunek konieczny, a nie wystarczający: bezczynne połączenia keep-alive, żądania plików statycznych i trwające handshake'i też trzymają zezwolenia, a żadnego z nich nie da się policzyć przy starcie.

Obniżenie MAX_CONNECTIONS to zwykła droga do tego ostrzeżenia, ponieważ domyślne QUEUE_CAPACITY jest wymiarowane z liczby workerów i nie podąża w dół — 7 workerów i MAX_CONNECTIONS=1000 daje 7 + 896 + 500 względem budżetu 1000. Zwróć uwagę, po którą gałkę sięgnąć: samo podniesienie MAX_CONNECTIONS nie usuwa go, dopóki QUEUE_MAX_WAITING pozostaje na wartości domyślnej, bo ta wynosi połowę MAX_CONNECTIONS i rośnie razem z nim — przy 7 workerach suma ustala się na 1799, a warunek znika dopiero od MAX_CONNECTIONS=1800 w górę. Obniż QUEUE_CAPACITY albo ustaw QUEUE_MAX_WAITING jawnie i dopiero potem podnoś budżet.

Dla puli dynamicznej (MIN:MAX) składnik wykonywanych to minimum — ta sama liczba, z której wymiarowane są wartości domyślne kolejki — więc pula rozrośnięta do maksimum trzyma więcej, niż mówi suma.

Porównanie liczy połączenia, podczas gdy budżet zużywają żądania, więc wdrożenie o dużym udziale HTTP/2 może zasadnie znajdować się powyżej niego: jedno połączenie niesie do H2_MAX_CONCURRENT_STREAMS żądań, więc tę samą zaległość trzyma ułamek połączeń. Tam ostrzeżenie jest oczekiwane. Duża pula na standardowym budżecie też je osiąga — automatycznie zwymiarowana pula 39 workerów daje 39 + 4992 + 4992 względem 10 000 — i na to warto zareagować, zamiast to ignorować.

Workery statyczne a dynamiczne

Ustaw PHP_WORKERS na pojedynczą liczbę, aby uzyskać stałą pulę:

bash
PHP_WORKERS=8 # Fixed 8 workers PHP_WORKERS=0 # Auto-detect: CPU / 2 (min 1)

Ustaw PHP_WORKERS na MIN:MAX, aby uzyskać automatyczne skalowanie:

bash
PHP_WORKERS=2:16 # Scale between 2 and 16 workers PHP_WORKERS=4:0 # 4 minimum, auto-detect maximum (CPU × 2) PHP_WORKERS=0:16 # auto-detect minimum (CPU / 4, min 1), 16 maximum

W trybie dynamicznym OxPHP zwiększa liczbę workerów, gdy wszystkie są zajęte, i zmniejsza ją, gdy workery pozostają bezczynne dłużej niż PHP_WORKERS_IDLE_SECONDS.

Tryb worker

Zmienna Wartość domyślna Opis
WORKER_MAX_MEMORY_MIB 0 Maksymalna pamięć w MiB na worker przed recyklingiem. 0 = bez limitu

Ustaw WORKER_MODE_ENABLED=true i skieruj ENTRY_FILE na swój skrypt bootstrap workera (np. ENTRY_FILE=worker.php lub ENTRY_FILE=../worker.php). Procesy PHP pozostają wtedy żywe pomiędzy żądaniami, utrzymując stan bootstrapu (autoloadery, połączenia z bazą danych) w pamięci. Workery są recyklowane automatycznie, gdy przekroczą WORKER_MAX_MEMORY_MIB, lub na żądanie, gdy aplikacja wywoła Worker::scheduleExit(). Parametr WORKER_MAX_REQUESTS z wcześniejszych wydań jest przestarzały i ignorowany — nie ustawiaj żadnego z nich lub przejdź na Worker::scheduleExit().

Przestarzałe: INDEX_FILE i WORKER_FILE

Starsze zmienne INDEX_FILE i WORKER_FILE są nadal parsowane w celu zachowania zgodności wstecznej. Gdy są ustawione, przy starcie emitują wpis logu WARN i są mapowane na nowy model:

Dawniej Odpowiednik dzisiaj
INDEX_FILE=index.php ENTRY_FILE=index.php
INDEX_FILE=index.html ENTRY_FILE=index.html
WORKER_FILE=/path/worker.php WORKER_MODE_ENABLED=true ENTRY_FILE=/path/worker.php

Jeśli ustawiono zarówno stare, jak i nowe zmienne, wygrywają ENTRY_FILE / WORKER_MODE_ENABLED. Migruj w dogodnym momencie; przestarzałe formy zostaną usunięte w przyszłym wydaniu.

SAPI / PHP

Zmienna Wartość domyślna Opis
SUPERGLOBALS_ENABLED true Wypełnia superglobalne PHP ($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER, php://input) przed wykonaniem skryptu. Ustaw na wartość fałszywą, aby pominąć wypełnianie — dane żądania są wtedy dostępne wyłącznie przez API obiektowe (oxphp_http_request()). Przydatne w aplikacjach, które korzystają bezpośrednio z API obiektowego i chcą uniknąć kosztu budowania superglobalnych przy każdym żądaniu

Limity czasu

Zmienna Wartość domyślna Opis
HEADER_TIMEOUT_SECONDS 5 Maksymalna liczba sekund na odebranie nagłówków HTTP po nawiązaniu połączenia (ochrona przed Slowloris)
DRAIN_TIMEOUT_SECONDS 25 Maksymalna liczba sekund oczekiwania na trwające połączenia podczas łagodnego zamknięcia

Czas wykonania PHP jest ograniczany przez własną dyrektywę ini max_execution_time (oraz set_time_limit() w czasie działania), a nie przez zmienną środowiskową OxPHP.

Ograniczanie liczby żądań

Zmienna Wartość domyślna Opis
RATE_LIMIT 0 (wyłączone) Maksymalna liczba żądań na IP w oknie czasowym. 0 wyłącza ograniczanie liczby żądań
RATE_WINDOW_SECONDS 60 Czas trwania okna ograniczania liczby żądań w sekundach

Bezpieczeństwo

Zmienna Wartość domyślna Opis
FRAME_OPTIONS SAMEORIGIN Ochrona przed clickjackingiem. SAMEORIGIN zezwala na osadzanie w ramkach tylko stronom z tego samego pochodzenia, DENY blokuje wszelkie osadzanie, off wyłącza (użyj, gdy osadzaniem zarządzasz przez własne CSP). Każda inna wartość wraca do domyślnego SAMEORIGIN z ostrzeżeniem przy starcie. Ustawia zarówno X-Frame-Options, jak i Content-Security-Policy: frame-ancestors na każdej odpowiedzi. Zobacz Ochrona przed clickjackingiem poniżej — emitowane wartości nagłówków, sposób, w jaki nagłówki serwera ustępują ustawionym przez aplikację, oraz wskazówki wyboru wartości
TRUSTED_PROXIES (nieustawione) Sieci zaufanych reverse proxy (CIDR-y rozdzielone przecinkami lub private). Gdy ustawione, OxPHP wyodrębnia rzeczywisty adres IP klienta z nagłówków Forwarded (RFC 7239) lub X-Forwarded-For, używając algorytmu rightmost-non-trusted. Przetwarza również X-Forwarded-Proto i X-Forwarded-Host na potrzeby $_SERVER['HTTPS'], REQUEST_SCHEME, SERVER_NAME i SERVER_PORT. Nieustawione = funkcja wyłączona
PHP_DENY_PATHS (nieustawione) Wzorce glob rozdzielone przecinkami, których pliki .php nigdy nie mogą być wykonywane przez bezpośredni URI (np. /uploads/**,/cache/**,/admin/legacy.php). Wzorce mogą wskazywać całe katalogi lub pojedyncze pliki. Obowiązuje w trybach bezpośredniego mapowania — Traditional i SPA; ignorowane z ostrzeżeniem przy starcie w trybach Framework i Worker, które nigdy nie wykonują dowolnych plików .php bezpośrednio. Obejmuje również skrypty osiągane przez rozwiązywanie indeksu katalogu (/uploads/uploads/index.php). W przypadku bezpośrednich URI .php dopasowanie następuje przed operacjami dyskowymi, więc zablokowane ścieżki dają tę samą odpowiedź niezależnie od tego, czy plik istnieje (brak wyroczni istnienia). Dawna nazwa PHP_DENY_DIRS jest akceptowana jako przestarzały alias i emituje WARN przy starcie. Zobacz Lista blokowania wykonywania PHP
PHP_DENY_FALLBACK 404 Co zwrócić przy dopasowaniu do PHP_DENY_PATHS. Albo status HTTP 400599 (współpracuje z ERROR_PAGES_DIR dla własnego HTML), albo ścieżka URI z przedrostkiem / do zapasowego skryptu PHP wewnątrz DOCUMENT_ROOT. Skrypt zapasowy otrzymuje OXPHP_DENIED_PATH i OXPHP_DENIED_PATTERN w $_SERVER. Walidowane przy starcie: skrypt musi istnieć, po kanonizacji mieścić się wewnątrz DOCUMENT_ROOT i sam nie może pasować do PHP_DENY_PATHS (zapobieganie pętlom)
SYMLINK_ALLOW_PATHS (nieustawione) Rozdzielona przecinkami lista ścieżek absolutnych, w obrębie których dowiązania symboliczne mogą wychodzić poza DOCUMENT_ROOT. Każdy wpis musi już istnieć na dysku; ścieżki względne i nieistniejące przerywają start. Nieustawione = brak zgody na wyjścia poprzez dowiązania symboliczne. Zobacz Dozwolone ścieżki dowiązań symbolicznych

Specjalna wartość private rozwija się do wszystkich prywatnych sieci RFC-1918, adresów loopback oraz link-local (IPv4 i IPv6): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, ::1/128, fc00::/7, fe80::/10.

Ochrona przed clickjackingiem

Clickjacking to atak, w którym wroga strona osadza Twoją witrynę w niewidzialnym <iframe> i podstępem skłania użytkownika do kliknięcia czegoś, czego nie widzi: przycisku „tak, usuń moje konto", zakupu jednym kliknięciem, monitu OAuth „autoryzuj". Obroną jest powiedzenie przeglądarce, kto — jeśli w ogóle ktokolwiek — może osadzać Twoje strony w ramkach. Kontroluje to FRAME_OPTIONS.

Osadzaniem rządzą dwa nagłówki — starszy X-Frame-Options (rozumiany przez wszystkie przeglądarki) i nowoczesny Content-Security-Policy: frame-ancestors (który ma pierwszeństwo tam, gdzie obecne są oba) — więc OxPHP emituje oba i polityka obowiązuje jednakowo w starych i nowych przeglądarkach. Pojedyncza wartość FRAME_OPTIONS mapuje się na dopasowaną parę:

FRAME_OPTIONS X-Frame-Options Content-Security-Policy Kto może osadzać Twoje strony
SAMEORIGIN (domyślnie) SAMEORIGIN frame-ancestors 'self' Tylko strony z tego samego pochodzenia
DENY DENY frame-ancestors 'none' Nikt, nawet Twoje własne strony
off (niewysyłany) (niewysyłany) Każdy — brak ograniczeń osadzania ze strony serwera

Wybór wartości. SAMEORIGIN jest wartością domyślną: blokuje osadzanie między pochodzeniami, na którym clickjacking faktycznie polega, wciąż pozwalając Twoim własnym stronom osadzać się nawzajem — co wiele aplikacji robi zasadnie (podglądy w panelu administracyjnym, widżety dashboardów, komponenty płatności hostowane na tym samym pochodzeniu). Wybierz DENY, gdy nic w Twojej witrynie nigdy nie ma być osadzane, nawet przez nią samą — dla najściślejszej postawy. Wybierz off tylko wtedy, gdy osadzaniem zarządzasz samodzielnie przez pełne Content-Security-Policy ustawiane przez aplikację — zobacz niżej.

Osadzanie przez zewnętrzne pochodzenia. Żadna wartość X-Frame-Options nie może wskazać konkretnego dozwolonego pochodzenia (ALLOW-FROM usunięto ze standardu). Aby pozwolić nazwanej stronie trzeciej osadzać Twoje strony, ustaw FRAME_OPTIONS=off i niech aplikacja emituje własne Content-Security-Policy z jawną listą frame-ancestors, np. header("Content-Security-Policy: frame-ancestors 'self' https://partner.example.com");.

Nagłówki aplikacji wygrywają. Nagłówki serwera są wartościami zapasowymi, stosowanymi tylko wtedy, gdy odpowiedź nie niesie takiego nagłówka: aplikacja, która ustawia własne X-Frame-Options lub Content-Security-Policy przez PHP header(), zachowuje je nietknięte. Oba nagłówki osadzania traktowane są jako jedna polityka, więc serwer nigdy nie zaprzecza aplikacji:

  • Jeśli Twoja aplikacja ustawia X-Frame-Options, OxPHP pomija swój zapasowy frame-ancestors (serwerowe CSP nadpisałoby wybór aplikacji w nowoczesnych przeglądarkach).
  • Jeśli Twoja aplikacja ustawia Content-Security-Policy zawierające dyrektywę frame-ancestors, OxPHP pomija swój zapasowy X-Frame-Options (ściślejszy serwerowy X-Frame-Options blokowałby za dużo w starszych przeglądarkach ignorujących CSP).

To samo pierwszeństwo dotyczy X-Content-Type-Options, które OxPHP ustawia na nosniff na każdej odpowiedzi: wartość ustawiona przez aplikację jest zachowywana dosłownie. Zwróć uwagę, że nosniff to jedyna wartość, która cokolwiek robi — aplikacja nadpisująca ją czymkolwiek innym po cichu wyłącza ochronę przed MIME-sniffingiem.

TLS

Zmienna Wartość domyślna Opis
TLS_CERT (nieustawione) Ścieżka do certyfikatu TLS zakodowanego w PEM. Aby włączyć TLS, muszą być ustawione zarówno TLS_CERT, jak i TLS_KEY
TLS_KEY (nieustawione) Ścieżka do klucza prywatnego TLS zakodowanego w PEM
TLS_MIN_VERSION 1.2 Minimalna akceptowana wersja protokołu TLS: 1.2 lub 1.3. Walidowana przy starcie (oraz przez oxphp config --check) nawet wtedy, gdy TLS nie jest włączony — każda inna wartość, w tym bajty spoza UTF-8, to twardy błąd startu. Pusta wartość jest traktowana jak nieustawiona

HTTP/2

Zmienna Wartość domyślna Opis
H2_MAX_CONCURRENT_STREAMS PHP_WORKERS_MAX × 4 (min 32) Maksymalna liczba jednocześnie otwartych strumieni na połączenie HTTP/2
H2_MAX_PENDING_RESET 20 Maksymalna liczba ramek RST_STREAM w kolejce, zanim połączenie zostanie zamknięte (ochrona przed Rapid Reset)
H2_MAX_HEADER_LIST_BYTES 65536 Maksymalna łączna liczba zdekodowanych bajtów nagłówków na żądanie
H2_KEEPALIVE_INTERVAL_SECS 20 Liczba sekund między ramkami PING HTTP/2; 0 wyłącza
H2_KEEPALIVE_TIMEOUT_SECS 10 Liczba sekund oczekiwania na odpowiedź PING przed zamknięciem połączenia

Pliki statyczne

Zmienna Wartość domyślna Opis
STATIC_MAX_AGE 30d Cache-Control: max-age dla plików statycznych. Akceptuje: 30s, 5m, 2h, 30d, 1w, 1y, same sekundy (3600) lub off, aby wyłączyć nagłówek. Zastępuje przestarzałą STATIC_CACHE_TTL.
STATIC_REVALIDATE off Wartość logiczna — zobacz Wartości logiczne. Ustaw na prawdę, aby włączyć rewalidację na podstawie mtime dla pamięciowego cache treści: czas modyfikacji pliku jest ponownie sprawdzany najwyżej raz na 3 sekundy na plik (nie na żądanie), a nieaktualne wpisy są automatycznie usuwane, dzięki czemu zmiany stają się widoczne w ciągu 3 sekund. Zastępuje przestarzałą STATIC_CACHE (gdzie off miało odwrotne znaczenie).
COMPRESSION_LEVEL 4 Jakość kompresji Brotli (0–11). 0 wyłącza kompresję

Logowanie

Zmienna Wartość domyślna Opis
LOG_LEVEL info Szczegółowość logów: trace, debug, info, warn, error
ACCESS_LOG (nieustawione) Rejestrowanie dostępu dla każdego żądania: all = każde żądanie, error = tylko 4xx/5xx, nieustawione = wyłączone
Note

ACCESS_LOG przyjmuje all lub error. Pozostaw nieustawione, aby całkowicie wyłączyć rejestrowanie dostępu.

Obserwowalność

Zmienna Wartość domyślna Opis
INTERNAL_ADDR (nieustawione) Adres serwera wewnętrznego (/health, /metrics, /config). Gdy nieustawione, serwer wewnętrzny nie jest uruchamiany. Wartość zawierająca tylko port (:9090 lub 9090) wiąże się z 127.0.0.1; ustaw jawnie 0.0.0.0:9090, aby udostępnić go poza hostem
INTERNAL_ALLOW_IPS (nieustawione) Lista dozwolonych CIDR/IP dla serwera wewnętrznego, rozdzielona przecinkami. Klient spoza listy otrzymuje 403 na /metrics, /config oraz ścieżkach wtyczek; sondy stanu (/health, /healthz, /readyz, /startupz oraz ich długie formy) pozostają osiągalne. Nieustawione/puste = zezwól wszystkim. Loopback nie jest domyślnie uwzględniony — dodaj 127.0.0.1/32 do listy, aby zachować dostęp z localhost. Nieprawidłowa lista przerywa start
ERROR_PAGES_DIR (nieustawione) Katalog zawierający własne strony błędów o nazwach {status}.html (np. 404.html, 503.html)
MAX_QUERY_BODY 524288 Maksymalny rozmiar ciała żądania w bajtach dla wewnętrznych endpointów zapytań (512 KiB)
TRACE_CONTEXT false Wartość logiczna — zobacz Wartości logiczne. Gdy prawda, włącza propagację W3C Trace Context: odczytuje nagłówki traceparent/tracestate i przekazuje je do PHP przez $_SERVER

OpenTelemetry

Zmienna Wartość domyślna Opis
OTEL_ENABLED false Włącza eksport spanów OpenTelemetry. Automatycznie ustawia TRACE_CONTEXT=true. Wartość logiczna — zobacz Wartości logiczne
OTEL_EXPORTER_OTLP_PROTOCOL grpc Protokół eksportu: grpc lub http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317 (gRPC) lub http://localhost:4318 (HTTP) Endpoint kolektora OTLP
OTEL_EXPORTER_OTLP_TIMEOUT 10000 Limit czasu eksportu w milisekundach
OTEL_EXPORTER_OTLP_HEADERS (nieustawione) Nagłówki uwierzytelniania: key=value,key2=value2
OTEL_SERVICE_NAME oxphp Nazwa usługi w eksportowanych spanach
OTEL_SERVICE_VERSION (nieustawione) Atrybut wersji usługi
OTEL_RESOURCE_ATTRIBUTES (nieustawione) Dodatkowe atrybuty zasobu: env=prod,region=us-east-1
OTEL_TRACES_SAMPLER parentbased_traceidratio Strategia próbkowania: always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG 1.0 Współczynnik próbkowania (0.0–1.0) dla samplerów opartych na współczynniku
Note

Nieprawidłowe lub wykraczające poza zakres wartości OTEL_TRACES_SAMPLER_ARG są przycinane do [0.0, 1.0] i logowane na poziomie warn. Nieznane wartości OTEL_TRACES_SAMPLER wracają do parentbased_traceidratio i są logowane.

APM

Zmienna Wartość domyślna Opis
OTEL_APM_ENABLED false Włącza APM: automatyczną instrumentację, przechwytywanie błędów oraz SDK śledzenia dla PHP. Wymaga OTEL_ENABLED=true. Wartość logiczna — zobacz Wartości logiczne
OTEL_APM_SLOW_QUERY_MS 100 Próg wolnego zapytania w milisekundach. Zapytania do bazy danych przekraczające ten próg otrzymują atrybut spanu oxphp.db.slow=true
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED false Zapisuje parametry wiązania w atrybucie spanu db.params. Wyłącz w środowisku produkcyjnym, jeśli parametry mogą zawierać dane wrażliwe. Wartość logiczna — zobacz Wartości logiczne
OTEL_APM_STACKTRACE_MAX_BYTES 8192 Maksymalny rozmiar w bajtach atrybutu exception.stacktrace. Po przekroczeniu limitu ślad stosu jest obcinany od końca znacznikiem …(truncated). 0 wyłącza obcinanie
OTEL_APM_MESSAGE_MAX_BYTES 4096 Maksymalny rozmiar w bajtach atrybutu exception.message (wartość domyślna odpowiada limitowi wartości na atrybut w New Relic). Po przekroczeniu limitu komunikat jest obcinany od końca znacznikiem …(truncated). 0 wyłącza obcinanie

Gdy APM jest włączone, OxPHP automatycznie podpina się pod 34 wewnętrzne funkcje PHP (PDO, mysqli, cURL, Redis, Memcached, operacje we/wy na plikach), aby tworzyć spany potomne. Funkcje PHP oxphp_apm_*() są rejestrowane niezależnie od tego, czy APM jest włączone — gdy jest wyłączone, są bezpiecznymi operacjami no-op.

Workery asynchroniczne

Zmienna Wartość domyślna Opis
ASYNC_WORKERS 0 (wyłączone) Liczba dedykowanych wątków workerów asynchronicznych. Gdy 0, funkcje asynchroniczne (oxphp_async itd.) są rejestrowane, ale przy wywołaniu rzucają OxPHP\Async\AsyncException. Ustaw wartość dodatnią, aby włączyć wykonywanie zadań w tle
ASYNC_QUEUE_CAPACITY ASYNC_WORKERS × 64 Maksymalna liczba oczekujących zadań w kolejce asynchronicznej. 0 = automatycznie (liczba workerów × 64)
ASYNC_MAX_FIBERS 256 Limit na worker liczby jednoczesnych Fiberów zadań asynchronicznych. Globalny dla procesu limit zadań w toku (w kolejce + wykonywanych) wynosi ASYNC_MAX_FIBERS × ASYNC_WORKERS; próba wysłania ponad ten limit jest natychmiast odrzucana z OxPHP\Async\AsyncException, aby kompozycja fan-out nie mogła doprowadzić do zakleszczenia

Pula workerów asynchronicznych obsługuje zadania w tle typu fire-and-forget wysyłane z PHP. Jest odrębna od puli workerów PHP i nie jest wymagana do standardowej obsługi żądań.

Nieprawidłowa wartość w którejkolwiek z tych trzech zmiennych (np. ASYNC_WORKERS=8x) to błąd startu — powrót do wartości domyślnej po cichu wyłączyłby lub błędnie skonfigurował pulę. Dokładnie pusta wartość jest traktowana jak nieustawiona.

Hooki runtime

Zmienna Wartość domyślna Opis
RUNTIME_HOOKS (wyłączone) Opcjonalna podmiana blokujących funkcji wbudowanych PHP na implementacje zawieszające fiber. 1/true/all włącza każdą kategorię hooków; lista rozdzielona przecinkami włącza konkretne kategorie (np. RUNTIME_HOOKS=sleep,streams)
Kategoria Co podpina
sleep Natywne sleep() i usleep() zawieszają bieżący fiber dokładnie tak jak oxphp_sleep()/oxphp_usleep()
streams Dwa oczekiwania zawieszają bieżący fiber, zamiast blokować wątek workera: blokujący odczyt ze strumienia gniazda tcp:// oraz stream_select(). Odczyt obejmuje klientów blokujących się na jednym gnieździe — fsockopen(), stream_socket_client(), wrappery strumieni HTTP, mysqlnd (PDO_MySQL, mysqli), phpredis; stream_select() obejmuje pętle czekające na kilka naraz. W obu wypadkach bez zmian w kodzie. Klienci czekający w inny sposób pozostają nietknięci (zobacz niżej)

Hooki działają wewnątrz fiberów żądań trybu worker i fiberów zadań asynchronicznych. Poza fiberem (kontekst żądania traditional/framework/SPA, CLI) zachowane jest oryginalne natywne zachowanie, łącznie z błędami walidacji argumentów. Z włączonymi hookami sleep kod zewnętrzny wywołujący sleep() przestaje blokować wątek workera — bez żadnych zmian w kodzie. Anulowanie zadania asynchronicznego podczas podpiętego uśpienia odwija je wyjątkiem OxPHP\Async\AsyncException, a podpięte sleep() zawsze zwraca 0 (wartość zwracana przy przerwaniu sygnałem w natywnej funkcji nie występuje).

Co obejmuje hook streams

Tym, co hook streams czyni kooperacyjnym, jest blokujący odczyt ze strumienia gniazda PHP oraz oczekiwanie wewnątrz stream_select(). Zanim na nim polegniesz, sprawdź, czy Twój klient czeka na jeden z tych dwóch sposobów. Kilka popularnych tego nie robi i dla nich nic się nie zmienia:

  • ext/curl. curl_exec() i curl_multi_* rozmawiają z gniazdami samodzielnie, poniżej strumieni PHP, więc hook nigdy ich nie widzi. Dotyczy to każdego klienta HTTP zbudowanego na curl — w tym Guzzle z domyślnym handlerem. Klienta curl można przestawić na handler wrappera strumieni, który przez strumienie PHP przechodzi.
  • socket_select(). To ext/sockets, inne API na surowych deskryptorach, i nie jest podpinane. stream_select() — jest. To samo dotyczy oczekiwania wewnątrz stream_socket_accept(), które również nie jest podpinane.
  • Strumienie z socket_export_stream(). Niosą inną tablicę operacji, prywatną dla PHP, której nie da się załatać z rozszerzenia.
  • Strumienie unix://, udp:// i udg://. Ten sam powód: ich tablice operacji są prywatne dla PHP.
  • ssl:// i tls:// po aktywacji szyfrowania. Zanim stream_socket_enable_crypto() się powiedzie, odczyty strumienia SSL są delegowane do zwykłego odczytu gniazda i dlatego zawieszają; od handshake'u — już nie.
  • Nawiązywanie połączeń i rozwiązywanie DNS. Klient trzymający otwarte połączenie zyskuje na każdym zapytaniu; ustanawianie połączenia — nie.
  • MySQL osiągany przez localhost. Klient MySQL odczytuje localhost jako „użyj gniazda uniksowego", które nie jest strumieniem tcp:// — zamiast tego wpisz w DSN 127.0.0.1. Dotyczy to tak samo PDO_MySQL i mysqli, i łatwo to przeoczyć, bo wszystko nadal działa, tylko bez korzyści.
  • Zapisy. Czekają tylko odczyty. Gotowość do odczytu utrzymuje się, dopóki jeden fiber jest właścicielem deskryptora i nic innego go nie opróżnia, podczas gdy miejsce w buforze wysyłania jest przyznawane i odbierane przez drugą stronę, więc fiber obudzony na możliwość zapisu może zastać okno ponownie zamknięte, zanim zapis się wykona — a wtedy PHP i tak blokuje wątek na pełny limit czasu. Zapis, który zapełnia bufor gniazda, zachowuje się więc dokładnie tak jak bez hooka. W praktyce kosztuje to niewiele: czasochłonne jest czekanie na odpowiedź, nie przekazanie zapytania jądru. (Zapisy są nadal oglądane, ale tylko w jednym celu: którego fibera wymianę połączenie aktualnie niesie — zobacz notkę o połączeniach współdzielonych niżej. Na połączeniu, którego nie używa żaden inny fiber — czyli na każdym połączeniu w zwykłym przypadku — zapis biegnie natywną ścieżką nietknięty.)

W tym zakresie hook zachowuje natywny kontrakt: limity czasu gniazd (stream_set_timeout(), default_socket_timeout) obowiązują bez zmian, odczyt po przekroczeniu limitu nadal raportuje timed_out przez stream_get_meta_data(), a tożsamość strumienia jest nietknięta, więc socket_import_stream() i podobne dalej działają. Jedno ograniczenie limitu czasu: termin jest sprawdzany tylko raz na tyknięcie schedulera, więc odpala najwcześniej przy następnym tyknięciu — w najlepszym razie 100 µs w trybie worker i 50 µs w puli async, a pod obciążeniem dłużej, bo tyknięcie trwa tyle, ile to, co worker akurat wykonuje. Pula async pauzuje dłużej, gdy nic się nie dzieje, ale nie dalej niż trzymany przez nią termin: termin odczytu lub zapisu skraca pauzę tak, by wypadła na nim samym, więc dłuższe czekanie nie opóźnia limitu. Przy default_socket_timeout równym 60 sekund w większości wdrożeń nie da się tego zaobserwować.

stream_select() jest podpinany tak, że hook czeka na deskryptorach wskazanych przez trzy tablice wywołania, a potem oddaje wywołanie PHP z limitem czasu ustawionym na zero, więc o wszystkim, co obserwowalne, nadal decyduje PHP: o zwracanej liczbie, przepisaniu tablic do samych gotowych strumieni, ostrzeżeniach i błędach argumentów. Oczekiwanie jest pomijane — a wywołanie biegnie dokładnie tak, jak bez hooka — ilekroć tablice zawierają coś, czego hook nie będzie zastępował: strumień do odczytu z danymi już zbuforowanymi (na który stream_select() odpowiada z bufora, nie patrząc na deskryptor), strumień bez deskryptora w ogóle, element niebędący żywym strumieniem (powiedzmy zamknięty, pozostawiony w tablicy — PHP odpowiada na to błędem, nie czekaniem), deskryptor, którego gotowości jądro w ogóle nie będzie obserwować (zwykły plik to typowy przypadek — liczy się jako gotowy w chwili zapytania), albo deskryptor równy lub większy od FD_SETSIZE, którego własny select() PHP odmawia wprost. To ostatnie to realny pułap, nie formalność: zajęty worker może trzymać ponad 1024 otwarte deskryptory, a stream_select() wskazujący któryś z nich zawodzi tak samo z hookiem, jak i bez niego.

Współdzielone połączenia między współbieżnymi fiberami

Jedno połączenie współdzielone między współbieżnymi fiberami jest bezpieczne dla klientów, których OxPHP chroni — i nic nie zyskuje. To normalny kształt aplikacji w trybie worker, a nie przypadek brzegowy: WordPress, Laravel i Symfony otwierają klientów bazy danych i cache raz przy starcie workera i podają te same każdemu żądaniu, a bez przepisania ich warstwy dostępu do danych nie da się od nich wymagać jednego połączenia na fiber.

Protokół klienta to sekwencja wymian — zapisz polecenie, odczytaj odpowiedź — i nic na połączeniu nie znaczy, gdzie jedna się kończy. Fiber zaparkowany na odczycie jest zaparkowany w środku wymiany, a polecenie drugiego fibera lądujące w tym miejscu łamie protokół. Obaj klienci zawodzą przy tym różnie i żaden dobrze: mysqlnd śledzi stan swojego połączenia i odmawia polecenia, zanim cokolwiek wyśle, a phpredis nie ma takiej kontroli i oba fibery czytają nawzajem swoje odpowiedzi — dane jednego żądania zwrócone innemu bez żadnego błędu.

Fiber zajmuje więc połączenie przed użyciem, na obu poziomach, na których musi się to wydarzyć: w operacjach gniazda, które utrzymują bajty w porządku, i w punktach wejścia klienta PDO i mysqli, bo odmowa mysqlnd następuje przed jakimkolwiek I/O i niczego na poziomie gniazda nie da się dosięgnąć na czas, by jej zapobiec. Inny fiber, który dotrze do tego samego połączenia, czeka na jego oddanie na poziomie klienta, gdzie trzymana jest wyłącznie tożsamość połączenia; na poziomie gniazda nie czeka, lecz otrzymuje odmowę — taką, jaką daje limit czasu gniazda — ponieważ fiber zawieszony wewnątrz operacji na cudzym strumieniu trzymałby wskaźnik, który właściciel może zwolnić. phpredis jest z dokładnie tego powodu chroniony również na poziomie klienta, metoda po metodzie. Tym, co poziom klienta zajmuje, jest samo połączenie, a nie obiekt PHP, który je trzyma, więc połączenie trwałe osiągane przez kilka obiektów PDO liczy się jako jedno, a połączenie otwarte przez PDO::connect() — które zwraca własną podklasę sterownika, a nie PDO — jest objęte jak każde inne.

Zysk z hooka odczytuj więc jako należący do połączeń, które fiber otwiera dla siebie: zadanie asynchroniczne wykonujące własne wywołania HTTP lub bazodanowe, żądanie otwierające własnego klienta. Tym, co dostaje połączenie współdzielone, jest odzyskany na czas oczekiwania wątek workera, dzięki czemu inne żądania mogą wykonywać pracę spoza tego połączenia; jego własne wymiany biegną jedna po drugiej, dokładnie tak jak z hookiem wyłączonym. Warto znać cztery granice:

  1. Fiber, który raz odpytał, trzyma połączenie do końca swojego żądania, bo koniec żądania to pierwszy moment na pewno leżący za końcem wymiany. Trzyma je także zaparkowany na czymkolwiek innym, więc żądanie, które odpytało, a potem czeka na własną pracę potrzebującą tego samego połączenia, czeka na siebie; oba rozchodzą się na ograniczeniu z punktu poniżej, zamiast postępować.
  2. Oczekiwanie jest zawsze ograniczone: mniejszą z wartości max_execution_time i default_socket_timeout. Nie ustawisz żadnej — a granicą jest 30 sekund, bo SAPI serwera przyjmuje wartości domyślne silnika: 30 dla pierwszej i 60 dla drugiej; 60 z default_socket_timeout obowiązuje tylko tam, gdzie max_execution_time wynosi 0. max_execution_time jest odczytywany tak, jak ma go bieżące żądanie, bo set_time_limit() to sposób, w jaki żądanie deklaruje, jak długo może działać; default_socket_timeout — tak, jak wystartował proces, bo to domyślny termin operacji na gnieździe, a żądanie zawężające go dla własnego wywołania — co biblioteki często robią i zostawiają po sobie — nie może skracać tej granicy żądaniom, które przyjdą po nim na tym samym workerze. Po przekroczeniu granicy wywołanie spada na niechronione zachowanie i mówi dlaczego w logu serwera: dla PDO i mysqli jest przekazywane klientowi, którego własna odmowa polecenia wydanego w środku wymiany jest błędem, który aplikacja już obsługuje, a dla phpredis — który takiej odmowy nie ma i zamiast tego odczytałby cudzą odpowiedź — rzuca RedisException i niczego nie wysyła. Konflikt na poziomie gniazda nie ma własnej granicy, bo nigdy nie czeka: operacja zawodzi od razu tak, jak zawodzi limit czasu, więc stream_get_meta_data() raportuje timed_out. Dwa fibery trzymające nawzajem to, na co czeka drugi, rozchodzą się więc na tej granicy, zamiast czekać na siebie w nieskończoność.
  3. Niektóre przypadki są celowo nieobjęte. Obiekt zapytania lub wyniku trzymany między żądaniami, którego nie poprzedza żadne zajmowane wywołanie: PDOStatement::execute() na zapytaniu przygotowanym we wcześniejszym żądaniu zachowuje się tak, jakby zajmowania nie było wcale. Konstruowanie drugiego uchwytu na połączeniu trwałym, gdy inny fiber jest na nim w środku wymiany: PDO sprawdza, czy połączenie z puli żyje, zanim je wyda, ta kontrola w środku wymiany zawodzi, a PDO odpowiada porzuceniem połączenia. Protokół napisany ręcznie na surowym gnieździe — zapisz polecenie, zawieś się na czymś innym, odczytaj odpowiedź później — bo poziom gniazda odmawia tylko wtedy, gdy trzymający jest zaparkowany na samej odpowiedzi, a między tymi dwoma punktami inny fiber przejmuje połączenie; trzej klienci powyżej są tu chronieni zajmowaniem na poziomie klienta, którego odpowiednika protokół ręczny nie ma. I wszystko, co sięga po połączenie w ogóle poza fiberem, jak destruktor uruchomiony przez cykliczny kolektor silnika między żądaniami, czego żadne zajmowanie nie widzi.
  4. Zamknięcie współdzielonego połączenia, gdy inne żądanie jest zaparkowane na jego odczycie, kończy to żądanie — statusem 500 i linią w logu nazywającą, co się stało. Zajmowanie utrzymuje dwie wymiany osobno; nie utrzyma połączenia przy życiu, a odpowiedź zaparkowanego żądania jest na połączeniu, które już nie istnieje — więc uczciwą odpowiedzią jest zakończyć je, a nie oddać to, co teraz zawiera zwolniona pamięć. Które wywołania mogą to zrobić, zależy od klienta, i tylko jedno z nich czeka: mysqli::close() i mysqli_close() są wywołaniami zajmowanymi, więc czekają na trzymającego i zamykają po wyczerpaniu tego oczekiwania, a Redis::close() też jest zajmowane, ale zamiast zamknąć, rzuca RedisException. Wszystko inne zamyka od razu — fclose() na surowym strumieniu i, co ważne, PDO, które w ogóle nie ma metody close(): połączenie zwalnia się przez porzucenie ostatniej referencji do uchwytu (unset($pdo), nadpisanie, wyjście z zakresu), a ta ścieżka biegnie wewnątrz własnego niszczenia obiektów silnika, gdzie żadne zajmowanie nie jest sprawdzane. Helper ponownego łączenia, który nadpisuje współdzielony uchwyt PDO, kończy więc żądania zaparkowane na starym połączeniu — bez ograniczonego oczekiwania, które dostaje jego odpowiednik mysqli. Żądanie dowiaduje się w chwili zamknięcia, a nie przy własnym terminie odczytu, którym dla mysqlnd byłby mysqlnd.net_read_timeout — fabrycznie doba. Aplikacje zamykające współdzielone połączenie, by wymusić ponowne połączenie (typowym przykładem jest $wpdb->check_connection() WordPressa), powinny oczekiwać, że żądania w tym momencie na nim zaparkowane zawiodą, a nie zwrócą złe dane.

Jeszcze jedna granica: uruchomienie hooka pod userlandowym schedulerem fiberów (AMPHP, Revolt) spada na blokujące I/O. Fiber wystartowany przez taki scheduler działa we własnym kontekście, którego scheduler OxPHP nie może wznowić, więc hook to wykrywa i bierze ścieżkę natywną, zamiast psuć którykolwiek ze schedulerów. Dotyczy to fibera, którego userlandowy scheduler startuje wewnątrz żądania. Własny fiber żądania jest tym, którym kieruje OxPHP, i w trybie worker jest prawdziwym FiberFiber::getCurrent() wewnątrz żądania go zwraca — więc biblioteki, które potrzebują jedynie odróżniać współbieżne żądania, działają bez żadnego spadku na ścieżkę natywną. Ten sam fakt jest powodem, dla którego Revolt odmawia uruchomienia swojej pętli zdarzeń z wnętrza żądania w trybie worker.

Włączanie i koszt

1, true i all włączają każdą kategorię, łącznie ze streams. Wdrożenie, które już ustawia RUNTIME_HOOKS=1 dla hooków sleep, zaczyna więc po aktualizacji podpinać gniazda bez żadnej własnej zmiany; wypisz kategorie jawnie (RUNTIME_HOOKS=sleep), jeśli nie tego chcesz. Włączenie streams łata też przy starcie w pamięci jeden wpis tablicy operacji strumieni PHP; strona pamięci jest przywracana do stanu, w jakim ją zastano, ale na platformie, gdzie jej pierwotnej ochrony nie da się ustalić, pozostaje zapisywalna — niewielka utrata hardeningu, raportowana w logu serwera.

Koszt, zmierzony: około 3–5 µs na rundę do gniazda i z powrotem na workerze, na którym nic innego nie jest zaparkowane, około 5–6 µs przy 64 fiberach zaparkowanych na deskryptorach i około 7–11 µs przy 200. Gotowość jest rozstrzygana przez zbiór, który jądro utrzymuje między oczekiwaniami, więc liczba ta śledzi, ile deskryptorów stało się gotowych, a nie ile czeka. Bezczynny worker czeka na tych deskryptorach, zamiast spać przez stały interwał i zauważać gotowość przy następnym tyknięciu, co ma ogromne znaczenie: ślepe spanie kosztowało około 2 ms na rundę.

Szerokie stream_select() niesie narzut, którego wąski przypadek nie ma, bo każdy deskryptor wskazany przez wywołanie jest rejestrowany przed oczekiwaniem i usuwany po nim. Zmierzony z pominięciem samego czekania — każdy deskryptor już gotowy do odczytu, więc wywołanie wraca od razu i zostaje sam narzut — wynosi około 6 µs wobec 5 µs bez hooka przy jednym deskryptorze, 56 µs wobec 9 µs przy 64 i 130–150 µs wobec 16–21 µs przy 200: mniej więcej 0,65 µs na deskryptor.

Odczytuj to jako stały koszt na wywołanie, a nie spowolnienie tej samej pracy. stream_select(), które faktycznie czeka — a po to pisze się pętlę select — przyćmiewa go: milisekunda oczekiwania sprawia, że nawet wynik dla 200 deskryptorów to około jedna dziesiąta wywołania, a tym, co kupuje, jest wątek workera, który niepodpięte wywołanie trzyma przez całe oczekiwanie. Dwa kształty są wyjątkiem i dla nich lepiej zostawić tę kategorię wyłączoną: wywołanie na wielu deskryptorach, które prawie nigdy nie czeka, bo nie ma czasu wątku do odzyskania; oraz żądanie, które jest pętlą zdarzeń, gdzie wątek i tak nie ma nic innego do roboty. Klienci czekający na jednym połączeniu — mysqlnd, phpredis, wrappery strumieni HTTP — siedzą na szczycie tabeli, gdzie narzut wynosi około mikrosekundy.

RUNTIME_HOOKS a oxphp_sleep()

Hook nie zastępuje własnego prymitywu — pokrywają różne przypadki:

  • Po oxphp_sleep() / oxphp_usleep() sięgaj w kodzie, który piszesz. Zawieszają fiber w trybie worker bezwarunkowo, bez żadnej flagi środowiska, a oxphp_sleep() przyjmuje ułamkowe sekundy (oxphp_sleep(0.25)) — precyzję, której natywne sleep() (całkowite sekundy) nie potrafi wyrazić.
  • RUNTIME_HOOKS=sleep włączaj dla kodu, którego nie możesz edytować — frameworka albo biblioteki zewnętrznej wywołującej natywne sleep()/usleep() bezpośrednio. Jest domyślnie wyłączony, działa tylko wewnątrz fibera i zachowuje natywny kontrakt (podpięte sleep() nadal przyjmuje int i zwraca 0).

Poleganie na RUNTIME_HOOKS we własnych handlerach wiąże ich kooperacyjność z ustawieniem wdrożenia zamiast z kodem; tam preferuj jawne oxphp_sleep().

Stan współdzielony

Wewnątrzprocesowe prymitywy współbieżności (OxPHP\Shared\Counter, Map, Channel, Mutex, Once, Pool, Atomic, Flag, Registry). Przewodnik po API znajdziesz w Stan współdzielony.

Zmienna Wartość domyślna Opis
SHARED_ENABLED true Wartość logiczna — zobacz Wartości logiczne. Główny przełącznik całego podsystemu OxPHP\Shared\*
SHARED_MAX_ENTRIES 100000 Globalny limit łącznej liczby wszystkich wpisów Shared. Wstawienie ponad ten limit kończy się CapacityException
SHARED_MAX_BYTES 1073741824 (1 GiB) Globalny limit szacowanej pamięci dla wszystkich wpisów Shared
SHARED_SOFT_LIMIT_RATIO 0.7 Rozpoczyna odrzucanie pracy o najniższym priorytecie, gdy zużycie przekroczy ten ułamek SHARED_MAX_BYTES / SHARED_MAX_ENTRIES
SHARED_METRICS_ENABLED true Wartość logiczna. Włącza lub wyłącza ekspozycję Prometheus oxphp_shared_*
SHARED_INTROSPECTION_ENABLED true Wartość logiczna. Włącza lub wyłącza API introspekcji /__ox_shared/* na serwerze wewnętrznym
SHARED_INTROSPECTION_PREVIEW_ENABLED true Wartość logiczna. Włącza lub wyłącza podglądy wartości w odpowiedziach introspekcji (wyłącz, gdy podglądy mogłyby ujawnić dane wrażliwe)
SHARED_CYCLE_DETECT_DEPTH 16 Głębokość BFS podczas sprawdzania cykli. Zwiększ dla głębokich, prawidłowych grafów
SHARED_CYCLE_DETECT_EDGES 10000 Liczba krawędzi przechodzonych podczas sprawdzania cykli. Zwiększ dla gęstych, prawidłowych grafów
SHARED_MAX_VALUE_SIZE 1048576 (1 MiB) Limit rozmiaru pojedynczej wartości. Wstawienie większej wartości kończy się natychmiastowym błędem
SHARED_MAX_CHANNEL_BYTES 67108864 (64 MiB) Limit łącznego ładunku na kanał
SHARED_POISON_STRICT false Wartość logiczna. Gdy prawda, panika wewnątrz domknięcia Mutex/Once trwale zatruwa prymityw zamiast próby odzyskania w miarę możliwości
SHARED_LOCK_DIAGNOSTICS off Diagnostyka rywalizacji o blokady: off, count lub trace
SHARED_LOCK_POLL_INTERVAL_MS 100 Interwał odpytywania używany przez sampler diagnostyki blokad
SHARED_PREVIEW_STRING_LIMIT 256 Obcinanie pojedynczego łańcucha znaków w podglądach /__ox_shared/preview, w bajtach (cięcie na granicy znaku)
SHARED_PREVIEW_ARRAY_LIMIT 20 Liczba wpisów próbkowanych w podglądach /entry?id=…

Profilowanie

Profiler próbkujący, który emituje ślady xhprof / speedscope. Formaty wyjściowe oraz integrację z przeglądarkami śladów opisano w Profilowanie.

Zmienna Wartość domyślna Opis
PROFILER_ENABLED false Wartość logiczna — zobacz Wartości logiczne. Główny przełącznik. Wszystkie pozostałe zmienne PROFILER_* są mimo to parsowane przy starcie, aby literówki ujawniały się natychmiast
PROFILER_SAMPLE_RATE 0.0 Prawdopodobieństwo (0.0–1.0), że żądanie zostanie spróbkowane. Wartości spoza zakresu są przycinane
PROFILER_INTERNAL false Wartość logiczna. Gdy prawda, żądania do serwera wewnętrznego (/health, /metrics, endpointy wtyczek) również kwalifikują się do próbkowania
PROFILER_AUTH_TOKEN (nieustawione) Opcjonalny token bearer. Gdy ustawiony, funkcje PHP oxphp_profiler_* wymagają, aby żądania niosły ten token, w celu włączenia profilowania na żądanie
PROFILER_MAX_SPANS 50000 Limit spanów profilu na żądanie. Profile przekraczające ten limit są obcinane
PROFILER_MAX_DEPTH 256 Maksymalna głębokość stosu wywołań przechwytywana na próbkę. Twardy limit to 65535
PROFILER_OUTPUT_DIR /tmp/oxphp-profiles Katalog na pliki profili zapisywane na dysku
PROFILER_OUTPUT_FORMATS xhprof,speedscope Rozdzielona przecinkami lista formatów wyjściowych zapisywanych na dysk
PROFILER_DISK_MAX_PER_SEC 10 Ograniczenie liczby plików profili zapisywanych na dysk na sekundę
PROFILER_RETENTION_COUNT 100 Maksymalna liczba plików profili przechowywanych w PROFILER_OUTPUT_DIR. Starsze pliki są usuwane
PROFILER_EXPORT_URL (nieustawione) Zdalny endpoint, do którego profile są wysyłane metodą POST. Gdy ustawiony, zapisy na dysk nadal następują, chyba że PROFILER_OUTPUT_FORMATS jest puste
PROFILER_EXPORT_FORMAT xhprof Format transmisji dla wysyłek PROFILER_EXPORT_URL
PROFILER_EXPORT_AUTH_TOKEN (nieustawione) Opcjonalny token bearer wysyłany z każdym żądaniem eksportu
PROFILER_EXPORT_XHGUI (automatyczne wykrywanie) Wartość logiczna. Wymusza opakowanie ładunku eksportu zgodne z XHGui. Nieustawione = automatyczne wykrywanie, gdy ścieżka PROFILER_EXPORT_URL kończy się na /run/import (wskazówki z hosta/query nie są dopasowywane)
PROFILER_EXPORT_BUGGREGATOR (automatyczne wykrywanie) Wartość logiczna. Wymusza kopertę Buggregator. Nieustawione = automatyczne wykrywanie, gdy ścieżka PROFILER_EXPORT_URL kończy się na /api/profiler/store. Koperta zawsze emituje xhprof, więc PROFILER_EXPORT_FORMAT jest dla niej ignorowane (wartość inna niż xhprof powoduje ostrzeżenie, nie jest krytyczna). Wzajemnie wykluczające się z PROFILER_EXPORT_XHGUI — włączenie obu to błąd startu
PROFILER_EXPORT_APP_NAME (nieustawione) app_name Buggregatora do grupowania projektów
PROFILER_EXPORT_TAGS (nieustawione) tags Buggregatora w postaci key=value,key2=value2; nieprawidłowy token, pusty klucz lub zduplikowany klucz to błąd startu

Przykładowe konfiguracje

Środowisko deweloperskie

bash
LISTEN_ADDR=127.0.0.1:8080 DOCUMENT_ROOT=./public LOG_LEVEL=debug ACCESS_LOG=all PHP_WORKERS=1 INTERNAL_ADDR=127.0.0.1:9090

Produkcja (Framework)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php PHP_WORKERS=8 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error MAX_CONNECTIONS=10000 INTERNAL_ADDR=127.0.0.1:9090 RATE_LIMIT=100 RATE_WINDOW_SECONDS=60 TRUSTED_PROXIES=private HEADER_TIMEOUT_SECONDS=5 DRAIN_TIMEOUT_SECONDS=25 COMPRESSION_LEVEL=4 STATIC_MAX_AGE=30d

Produkcja (tryb worker)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public WORKER_MODE_ENABLED=true ENTRY_FILE=../worker.php PHP_WORKERS=8 WORKER_MAX_MEMORY_MIB=128 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error INTERNAL_ADDR=127.0.0.1:9090

TLS

bash
LISTEN_ADDR=0.0.0.0:443 TLS_CERT=/etc/ssl/oxphp/cert.pem TLS_KEY=/etc/ssl/oxphp/key.pem DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php

Sprawdzanie aktywnej konfiguracji

Gdy serwer wewnętrzny działa, odpytaj endpoint /config, aby zobaczyć rozwiązaną konfigurację:

bash
curl -s http://localhost:9090/config | jq .
json
{ "listen_addr": "0.0.0.0:80", "document_root": "/var/www/html/public", "entry_file": "/var/www/html/public/index.php", "log_level": "warn", "executor_type": "sapi", "php_workers": "8", "tokio_workers": 4, "queue_capacity": 1024, "queue_wait_timeout_ms": 1000, "queue_max_waiting": 1024, "queue_max_waiting_bytes": 67108864, "max_connections": 10000, "drain_timeout_seconds": 30, "header_timeout_seconds": 5, "rate_limit": 100, "rate_window_seconds": 60, "tls_enabled": true, "compression_level": 4, "access_log": "all", "max_query_body": 524288, "worker_mode_enabled": false, "worker_max_memory_mib": 0, "static_max_age": 2592000, "static_revalidate": false, "async_workers": 0, "async_queue_capacity": 0, "async_max_fibers": 256, "async_in_flight_cap": 0, "trace_context": true, "superglobals_enabled": true, "trusted_proxies": false, "plugins": { "otel": { "enabled": true, "protocol": "grpc", "service_name": "oxphp" }, "apm": { "enabled": true, "slow_query_ms": 100, "db_capture_params": false, "hooks_registered": 34 } } }
Note

Serwowana odpowiedź /config usuwa kilka kluczy, które niesie wewnętrzna reprezentacja Config: ścieżki certyfikatu i klucza TLS nigdy nie są emitowane (tls_enabled wskazuje, czy TLS jest aktywny), a internal_addr oraz error_pages_dir są usuwane — to topologia wdrożenia i ścieżki w systemie plików, które pomagają atakującemu, a nie są potrzebne narzędziom zbierającym metryki.

Zobacz również

Znalazłeś błąd? Zgłoś go →