Dokumentacja konfiguracji

OxPHP konfiguruje się za pomocą zmiennych środowiskowych oraz jednego opcjonalnego pliku: .oxphpdeny, czyli reguł blokowania ścieżek odczytywanych ze szczytu DOCUMENT_ROOT. 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). Zobacz Wymiarowanie asynchronicznego środowiska wykonawczego

Wymiarowanie asynchronicznego środowiska wykonawczego

TOKIO_WORKERS domyślnie wynosi połowę dostępnych rdzeni, a pozostawiona połowa nie stoi bezczynnie: należy do puli workerów PHP. Oba zestawy wątków rywalizują o te same rdzenie, więc środowiska wykonawczego nie da się poszerzyć bez odebrania cykli aplikacji.

Podniesienie go do pełnej liczby rdzeni to strata wszędzie tam, gdzie pula jest już zwymiarowana blisko tej liczby. Zmierzone na maszynie 4-rdzeniowej z pulą 4 workerów serwującą skompresowane odpowiedzi: przejście z 2 wątków środowiska wykonawczego na 4 kosztowało 13% przepustowości przy ciele JSON o rozmiarze 1,6 KB i 26% przy ciele 5 KB, a przy tym zużyło więcej CPU, bo wątków było więcej niż rdzeni.

Podnoś go tam, gdzie proporcje są inne: przy puli małej w stosunku do maszyny albo przy pracy ograniczonej przez I/O, a nie przez PHP (upstreamy za proxy, pliki statyczne, odpowiedzi strumieniowe), gdzie pracę wykonuje środowisko wykonawcze, a pula głównie czeka. Po każdej zmianie obserwuj pulę, a nie tylko przepustowość: oxphp_busy_workers względem oxphp_workers_current pokazuje, do której strony trafiły rdzenie. Przepustowość rosnąca przy coraz bardziej zajętej puli to realny zysk; przepustowość rosnąca przy opróżniającej się puli zwykle oznacza, że praca się przeniosła, a nie że wykonano jej więcej.

Tip

TOKIO_WORKERS i PHP_WORKERS domyślnie wynoszą po połowie dostępnych rdzeni, więc ich suma wypada na liczbie rdzeni. Zmień jedno, a suma się przesunie — dlatego każde z nich warto zmierzyć, zamiast zgadywać.

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 Najdłuższy czas, przez jaki żądanie może czekać na workera PHP, zanim zostanie odrzucone z 529. To pułap, a nie stała wartość. 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. Pod przeciążeniem serwer sam skraca budżet poniżej tego pułapu i wydłuża go z powrotem, gdy kolejka się opróżni; oxphp_admission_wait_budget_us publikuje budżet obowiązujący, a oxphp_admission_wait_budget_ceiling_us skonfigurowany (zobacz Adaptacyjny budżet oczekiwania). Termin jest egzekwowany z obu stron: gdy upłynie, żądanie dostaje odpowiedź 529, a worker, który dotrze do żądania po terminie, odmawia go zamiast wykonać, więc żądanie w kolejce, którego nikt nie podejmuje, nie czeka w nieskończoność. 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; usuwa to oczekiwanie na miejsce w kolejce, ale nie oczekiwanie wewnątrz kolejki. Obniż go, gdy aplikacja wywołuje przez HTTP ten sam serwer (wewnętrzne wywołanie potrzebuje workera, którego trzyma zewnętrzne, 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; ten, który zamknie je, będąc już w kolejce, zachowuje swoje miejsce, aż dotrze do niego worker albo budżet się wyczerpie, i dostaje wtedy odpowiedź 499, zamiast zostać policzony jako odmowa z przeciążenia. Czego serwer nie widzi, to klient, który przestaje czekać bez zamykania. Taki klient trzyma 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.

Tempo obsługi nie jest znane przy starcie, więc wartość domyślna nie może tego policzyć i 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, handlerze 200 ms i domyślnym pułapie 1 s (budżecie obowiązującym zawsze, gdy pula nie zrzuca ruchu) 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ę. B oznacza tu budżet obowiązujący, którym pod przeciążeniem jest ten skrócony przez serwer, a nie skonfigurowany pułap. Zanim zwymiarujesz zbiór oczekujących na podstawie liczby z pliku konfiguracyjnego, odczytaj oxphp_admission_wait_budget_us.

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ąć.

Adaptacyjny budżet oczekiwania

QUEUE_WAIT_TIMEOUT_MS to pułap. Dopóki praca, którą wpuszcza oczekiwanie, kończy się dla klientów, którzy już odeszli, serwer sam skraca budżet, i to gwałtownie, co warto wiedzieć przed wyborem pułapu:

  • W dół: budżet jest zmniejszany o połowę za każdym razem, gdy co najmniej ćwierć tego, co pula uruchomiła, trafiła do klientów, którzy już odeszli, mierzone w oknie co najmniej ośmiu uruchomionych żądań, a nie w stałym interwale.
  • Dno: jedna sześćdziesiąta czwarta pułapu (15,6 ms przy wartości domyślnej) albo 10 ms, zależnie od tego, co większe, choć nigdy powyżej samego pułapu.
  • W górę: wraca krokami po jednej ósmej pułapu i tylko wtedy, gdy kolejka jest obserwowana jako pusta, a do klientów, którzy odeszli, trafiła mniej niż ćwierć tego, co pula uruchomiła. Zwykły drobny strumień zamkniętych kart i limitów czasu klientów nie trzyma go na dole, a serwer, którego kolejka nigdy się nie opróżnia, zostaje na skróconym budżecie, dopóki obciążenie nie odpuści.

Operator, który ustawił 1000, powinien spodziewać się, że w trakcie takiego epizodu obowiązuje około 15, więc alerty dotyczące oczekiwania pisz względem pary oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us, a nie względem skonfigurowanej liczby.

Skracanie napędzają klienci, którzy zamykają połączenie. Populacja, która przestaje czekać bez zamykania, nie daje żadnego sygnału i pozostawia w mocy pułap.

Wywołania do samego siebie

Aplikacja, która wywołuje przez HTTP ten sam serwer, marnuje swoje oczekiwania bez względu na ich długość: wewnętrzne wywołanie potrzebuje workera, którego trzyma zewnętrzne. oxphp_admission_wait_wasted_total liczy takie oczekiwania, ale tylko wtedy, gdy budżet jest na pułapie. Tam, gdzie budżet zszedł w dół, licznik milknie, więc 0 na nim samo w sobie nie dowodzi, że tego wzorca nie ma. To, czy budżet schodzi w dół, nie jest też właściwością samego wywołania do siebie: aplikacja wywołująca samą siebie, której własni klienci czekają hojnie (wewnętrzny cron, sidecar z długim limitem czasu), zachowuje swój pułap.

QUEUE_WAIT_TIMEOUT_MS=0 tego nie naprawia. Usuwa oczekiwanie na miejsce w kolejce, a nie oczekiwanie wewnątrz kolejki, więc jeśli QUEUE_CAPACITY nie jest na tyle małe, by kolejka była pełna, wewnętrzne wywołanie zostaje przyjęte do kolejki, której nikt nie konsumuje, i nigdy nie dostaje odpowiedzi.

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.

To zablokowanie jest logowane. Przyjęcie, które musi czekać na zezwolenie, loguje WARN wskazujący budżet, a INFO podające, jak długo pętla stała, zostaje zapisane w chwili nadejścia zezwolenia. Komunikaty mają ograniczoną częstotliwość, by serwer oscylujący wokół pułapu nie zamienił własnego logu w awarię: zablokowanie zaczynające się w ciągu kilku sekund od zgłoszonego jest początkowo wstrzymywane i zgłaszane po zamknięciu tego okna, jeśli nadal trwa; takie, które kończy się wewnątrz okna, nie zostawia żadnej linii. Metryki widzą każde: oxphp_accept_stalled (wskaźnik, czytelny przez cały czas zablokowania, bo wewnętrzny listener nie przechodzi przez MAX_CONNECTIONS) i oxphp_accept_stalls_total (licznik); zobacz dokumentację metryk. Nadchodzący klienci nadal nie dostają żadnej odpowiedzi zamiast błędu, ale log i metryki odnotowują zablokowanie.

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.

Jak wykrywana jest liczba procesorów

Każda wartość domyślna na tej stronie wyrażona w CPU (TOKIO_WORKERS oraz PHP_WORKERS nieustawione, równe 0 albo z 0 po którejkolwiek stronie MIN:MAX) liczy procesory, na których proces może działać: jego maskę koligacji CPU (taskset, docker run --cpuset-cpus), obniżoną do limitu CPU kontenera, jeśli taki ustawiono (docker run --cpus; cpu.max w cgroup v2, cpu.cfs_quota_us w v1). Limit jest zaokrąglany w dół i liczy się jako co najmniej 1, więc --cpus=1.5 liczy się jako 1.

Dwa rodzaje ograniczeń nie są liczone, a kontener powstrzymywany tylko przez jedno z nich jest wymiarowany ze wszystkich CPU, które widzi:

  • Waga CPU: docker run --cpu-shares, cpu.weight w cgroup v2, cpu.shares w v1. Decyduje o tym, jak sporny czas CPU jest dzielony z sąsiadami. Nie jest pułapem i nie wynika z niej żadna liczba CPU.
  • Limit egzekwowany poza kontenerem. Platforma może sprzedawać część maszyny i egzekwować to tam, gdzie kontener tego nie widzi. Na przykład na DigitalOcean App Platform zaobserwowano instancję sprzedawaną jako 1 vCPU, która działała w piaskownicy gVisor bez limitu (cpu.cfs_quota_us wynosi -1) i z 8 widocznymi CPU, więc wartości domyślne dały 4 workery PHP, 4 wątki środowiska wykonawczego i kolejkę 512.
Plany, które egzekwują limity poza zasięgiem wzroku

W takim planie ustaw PHP_WORKERS i TOKIO_WORKERS jawnie, na tyle, ile plan faktycznie zapewnia. W trybie worker sprawdź też PHP_WORKERS × WORKER_MAX_MEMORY_MIB względem pamięci planu. Ten próg to wyzwalacz recyklingu, który worker sprawdza w swojej pętli, a nie pułap tego, co może zaalokować jego sterta PHP, więc pula może trzymać tyle i więcej, zanim którykolwiek worker zostanie z tego powodu zrecyklowany, a do tego dochodzą OPcache i sam serwer; przy domyślnym 0 progu nie ma wcale. Ta sama piaskownica może ukrywać także limit pamięci: opisany wyżej kontener App Platform nie widział żadnego.

Przy starcie serwer loguje tę liczbę, to, co ją ustaliło, oraz wynikające z niej PHP_WORKERS i TOKIO_WORKERS. Tak to wygląda pod docker run --cpus=1 na maszynie z 14 CPU:

json
{"timestamp":"2026-10-05T18:55:39.801254Z","level":"INFO","fields":{"message":"CPU count for default sizing","cpus":1,"cpu_source":"cgroup quota","host_cpus":14,"php_workers":"1 (auto)","tokio_workers":"1 (auto)"}}
  • cpu_source ma wartość host, gdy ani maska, ani limit nie obniżyły liczby, affinity lub cgroup quota, gdy zrobiło to jedno z nich, oraz unknown, gdy serwer nie mógł tego ustalić, bo nie dało się odczytać maski albo liczby CPU online. Jeśli w ogóle nie mógł policzyć CPU, cpus wynosi 4.
  • host_cpus to liczba CPU, które system zgłasza jako online (/sys/devices/system/cpu/online w Linuksie); maska ani limit jej nie obniżają. Jest pomijane, gdy nie da się go odczytać. Środowisko uruchomieniowe kontenerów, które przepisuje ten plik tak, by wymieniał tylko CPU kontenera, sprawia, że zawężona maska odczytuje się jako host.
  • (auto) oznacza wartość pozostawioną całkowicie wartości domyślnej: zmienna jest nieustawiona lub pusta albo, w przypadku TOKIO_WORKERS, nie jest liczbą.

cpu_source równe host na planie, który sprzedał Ci mniej CPU niż cpus, to opisany wyżej przypadek.

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). Workery 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 albo obsłużą trzy kolejne żądania, które się rozpadły, oraz na żądanie, gdy aplikacja wywoła Worker::scheduleExit(); co wlicza się do tego trzeciego, opisuje Recykling. 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 Opisuje żądanie dla PHP, tak by przed wykonaniem skryptu zbudowało $_SERVER i $_GET. Ustaw na wartość fałszywą, aby pominąć tę pracę w aplikacji, która nie czyta żadnej z nich. Dotyczy tylko tych dwóch: $_POST, $_FILES i $_COOKIE są budowane z ciała żądania i nagłówka Cookie, które PHP otrzymuje tak czy inaczej, a php://input to strumień, a nie superglobalna. Pełną tabelę znajdziesz w SUPERGLOBALS_ENABLED

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. Żądanie, które w chwili terminu tkwi w blokującym wywołaniu, może przytrzymać proces dłużej (zobacz Łagodne zamknięcie)

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. To ograniczenie nie sięga zadań asynchronicznych: wątek puli ASYNC_WORKERS nie ma terminu wykonania, więc ani dyrektywa, ani set_time_limit() nie ograniczają tego, co wykonuje zadanie zlecone przez oxphp_async(). Takie zadanie ograniczaj zamiast tego argumentem limitu czasu oxphp_async_await(); zobacz Asynchroniczne promisy.

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: rozdzielona przecinkami mieszanka CIDR-ów, pojedynczych adresów IP oraz słów kluczowych private i cloudflare (np. private,cloudflare). 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; przy cloudflare żądanie, którego łańcuch przekazywania pokazuje, że przyszło z węzła brzegowego Cloudflare, bierze go zamiast tego z CF-Connecting-IP. 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 do reguły blokującej .oxphpdeny. Albo status HTTP 400–599 (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). Reguła .oxphpdeny może obejmować skrypt; w trybie worker reguły blokujące .oxphpdeny przyjmują wyłącznie kod statusu. Zobacz Reguły blokowania ścieżek
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).

Kompresja

Zmienna Wartość domyślna Opis
COMPRESSION_ENCODINGS br,zstd,gzip Kodowania treści oferowane przez serwer, rozdzielone przecinkami: br (lub brotli), zstd, gzip. Nieznana nazwa to błąd startu. Ustaw całą wartość na off (lub none), aby całkowicie wyłączyć kompresję. Nie jest to element listy, więc br,off to błąd, a nie przełącznik. Puste przypisanie przywraca wartość domyślną. Kolejność zapisana tutaj jest ignorowana: buforowane pliki statyczne preferują Brotli, którego koszt ponosi się raz, a wszystko inne preferuje Zstandard. Zobacz Kompresja
COMPRESSION_BROTLI_LEVEL 5 Jakość Brotli (od 0 do 11). Zastępuje przestarzałą COMPRESSION_LEVEL
COMPRESSION_ZSTD_LEVEL 6 Poziom kompresji Zstandard (od 0 do 19)
COMPRESSION_GZIP_LEVEL 6 Poziom kompresji gzip (od 0 do 9)
COMPRESSION_LEVEL (nieustawione) Przestarzała nazwa COMPRESSION_BROTLI_LEVEL; emituje WARN przy starcie. COMPRESSION_LEVEL=0 zachowuje dawne znaczenie i wyłącza wszystkie kodowania, nie tylko Brotli. Jawne COMPRESSION_BROTLI_LEVEL wygrywa z wartością niezerową, ale nie z 0, które wyłącza kompresję bez względu na resztę ustawień

Kodowanie jest oferowane, gdy COMPRESSION_ENCODINGS je wymienia i jego poziom nie wynosi 0; każdy z tych warunków z osobna je wycofuje. Poziom spoza zakresu albo niebędący liczbą przerywa start błędem wskazującym zmienną. Wartości nie są przycinane.

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.

Klient, który zamknie połączenie, zanim serwer odpowie, jest logowany na poziomie debug, a nie error. To najczęstsze zdarzenie, jakie widzi publiczny listener (anulowany fetch, zamknięta karta, proxy, któremu upłynął limit czasu odczytu), i nie jest awarią serwera, więc przy domyślnym LOG_LEVEL jest niewidoczne. Zliczaj je za pomocą oxphp_request_cancelled_total{reason="client_abort"} albo podnieś LOG_LEVEL=debug, aby zobaczyć poszczególne połączenia. Limity czasu egzekwowane przez sam serwer (HEADER_TIMEOUT_SECONDS) pozostają na poziomie warn, a każda inna awaria połączenia na poziomie error.

LOG_LEVEL nie filtruje logu dostępu

Te dwa są niezależne. LOG_LEVEL ustala szczegółowość własnej diagnostyki serwera, więc LOG_LEVEL=warn z ACCESS_LOG=error raportuje ostrzeżenia i błędy oraz loguje każde żądanie 4xx/5xx. Samo wyciszenie logu dostępu wymaga dyrektywy RUST_LOG wskazującej jego target: RUST_LOG=warn,access_log=off, gdzie warn zastępuje taki LOG_LEVEL, jakiego używałeś. RUST_LOG, gdy jest ustawione i da się je sparsować, zastępuje LOG_LEVEL, a nie nakłada się na nie. Zobacz 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) 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. Przy http/protobuf jest to bazowy URL: spany są wysyłane pod /v1/traces względem niego
OTEL_EXPORTER_OTLP_TIMEOUT 10000 Limit czasu eksportu w milisekundach. Przy gRPC ogranicza też łączenie się z kolektorem, łącznie z uzgadnianiem TLS, więc utrzymuj go powyżej czasu, jaki to zajmuje; próba, która musi najpierw nawiązać połączenie, może trwać do dwukrotności limitu. Przy gRPC w Linuksie ustala też, jak szybko zamykane jest połączenie, które przestało przyjmować dane — zobacz Błędy eksportu spanów w logu
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, funkcje spanów są operacjami no-op zwracającymi wartości wartownicze. Wyjątkiem jest oxphp_apm_trace(): nadal uruchamia swój callback i zwraca jego wartość, więc wyłączenie APM nigdy nie zmienia tego, co oblicza Twoja aplikacja.

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(), także w kontekstach, w których te dwie również nie mogą zawiesić i zamiast tego czekają na wątku workera (zobacz Gdzie hooki sleep nie zawieszają)
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)
net Oczekiwania poniżej strumieni PHP, gdzie hook odczytu streams ich nie widzi, zawieszają bieżący fiber, zamiast blokować wątek workera: nawiązywanie połączenia przez stream_socket_client(), fsockopen() i każdego klienta zbudowanego na strumieniach PHP (w tym mysqlnd, a więc PDO_MySQL i mysqli, oraz phpredis), uzgadnianie TLS, odczyty ze strumieni ssl:///tls:// po aktywacji szyfrowania, zapisy, które muszą czekać na miejsce w buforze wysyłania, oraz odczyty strumieniowe z gniazd unix://, udp:// i udg://, ze strumieni stream_socket_pair() oraz ze strumieni, które socket_export_stream() tworzy z gniazd innych niż TCP. Wymaga wymienienia obok niej streams i działa tylko w Linuksie (zobacz Kiedy kategoria net pozostaje wyłączona). Nieobjęte: oczekiwania wewnątrz libcurl, libpq (pgsql, pdo_pgsql) i libmemcached, które wywołują poll() z własnych bibliotek; rozwiązywanie DNS; stream_socket_recvfrom(); zapisy na zwykłym (nie-TLS) strumieniu z nieskończonym limitem czasu (default_socket_timeout=-1); oraz trwałe nawiązywanie połączeń (pfsockopen(), STREAM_CLIENT_PERSISTENT i tryby trwałe klientów na nich zbudowanych)

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 wszędzie tam, gdzie fiber da się zawiesić (a nie wszędzie się da; zobacz niżej), w obu wypadkach bez zmian w kodzie. Anulowanie zadania asynchronicznego podczas podpiętego uśpienia odwija je wyjątkiem OxPHP\Async\AsyncException, a podpięte sleep(), które zawiesza, zwraca 0; wartość zwracana przy przerwaniu sygnałem w natywnej funkcji pojawia się tylko na ścieżce blokującej opisanej niżej.

Sprawdzanie, czy ustawienie zostało przyjęte

Serwer raportuje zainstalowane kategorie, jako sparsowany zbiór, w dwóch miejscach. Na serwerze wewnętrznym:

Installed hook categories
$ curl -s localhost:9090/config | jq .runtime_hooks [ "sleep", "streams", "net" ]

Oraz z PHP, dla wdrożenia, którego serwer wewnętrzny jest nieosiągalny lub niewłączony:

php
<?php echo json_encode(oxphp_server_info()['runtime_hooks']); // ["sleep","streams","net"]

Oba odczytują tę samą wartość i raportują zbiór, a nie łańcuch: RUNTIME_HOOKS=all, RUNTIME_HOOKS=1 i RUNTIME_HOOKS=sleep, streams, net wracają jako ["sleep","streams","net"]. Pusta lista oznacza, że nie zainstalowano żadnego hooka. Taka jest odpowiedź dla nieustawionej zmiennej, dla RUNTIME_HOOKS=0, dla wartości wymieniającej wyłącznie kategorie nieznane tej kompilacji oraz dla błędnie zapisanej nazwy zmiennej. Wartość łącząca oba rodzaje to co innego: RUNTIME_HOOKS=sleep,bogus odczytuje się jako ["sleep"], więc niepusta lista sama w sobie nie dowodzi, że każda nazwa została zrozumiana. Każda nierozpoznana nazwa dostaje przy starcie własną linię w logu serwera i ta linia jest jedynym miejscem, w którym jest wymieniona. Sprawdzaj po każdej zmianie ustawienia: nierozpoznana zmienna środowiskowa nie jest nigdzie błędem, więc „literówka" objawia się inaczej tylko jako opóźnienie.

Zainstalowane to nie to samo co działające

Lista raportuje, którym funkcjom wbudowanym podmieniono handlery, co dzieje się przy starcie niezależnie od trybu routingu. Tam, gdzie w ogóle nie ma fiberów, podmiany delegują do handlerów, które zastąpiły, więc serwer w trybie traditional raportuje ["sleep","streams","net"], zachowując się przy tym w pełni natywnie. Proces CLI php raportuje ["sleep","streams"]: silnik jest częścią tego pliku wykonywalnego, więc net nie potrafi odróżnić oczekiwań PHP od własnych oczekiwań programu i pozostaje wyłączona. Czytaj listę jako „ten proces jest przygotowany na hooki", a tryb routingu jako to, czy cokolwiek może z nich skorzystać.

W obrębie streams jest jeszcze węziej: część dotycząca odczytu z gniazd może nie zainstalować się samodzielnie, na platformie, gdzie tablicy operacji strumieni nie da się uczynić zapisywalną, podczas gdy stream_select() i punkty wejścia baz danych pozostają podpięte. Kategoria jest raportowana tak czy inaczej; jedynym sygnałem jest linia w logu serwera mówiąca, że odczyty z gniazd pozostają blokujące. Ta linia, podobnie jak ta o nierozpoznanej kategorii i linie zapisywane przez net (niżej), podlega logowaniu błędów PHP: przy nieustawionym error_log (domyślnie) trafia do własnego logu serwera jako warn, a LOG_LEVEL=error ją ukrywa.

Kiedy kategoria net pozostaje wyłączona

net jest raportowana tylko wtedy, gdy faktycznie działa. Wymieniona bez streams pozostaje wyłączona, a mówi o tym linia w logu serwera. Na platformie innej niż Linux, tam, gdzie przy starcie nie da się odróżnić własnego kodu serwera od kodu PHP, albo tam, gdzie poll() używane przez PHP nie jest powiązane z tym, które przechwytuje serwer, pozostaje wyłączona z linią mówiącą, że nawiązywanie połączeń i oczekiwania TLS pozostają blokujące. Brak net na liście po tym, jak o nią poprosiłeś, jest więc odpowiedzią, a nie luką w raporcie.

Jedyne, czego lista nie mówi, to gdzie znajduje się OpenSSL. Oczekiwania OpenSSL (uzgadnianie TLS, łącznie z tym na końcu nawiązywania połączenia ssl://, oraz odczyty i zapisy na strumieniu TLS po aktywacji szyfrowania) są parkowane tylko wtedy, gdy OpenSSL jest wbudowany w PHP, jak w oficjalnych obrazach. Przy OpenSSL ładowanym jako osobne rozszerzenie współdzielone (extension=openssl) te oczekiwania z punktu widzenia serwera nie są własnymi oczekiwaniami PHP, więc trzymają wątek, podczas gdy nawiązywanie połączeń, unix:// i pozostałe odczyty nadal zawieszają, a net jest nadal raportowana.

Gdzie hooki sleep nie zawieszają

Tam, gdzie przełączenie fibera jest zabronione, podpięte sleep()/usleep() przekazują wywołanie funkcji wbudowanej, którą zastąpiły, a ta czeka na wątku workera i zwraca to, co zwraca. sleep() przerwane tam sygnałem oddaje więc sekundy, których nie przespało, tak jak poza fiberem. (Kontrola argumentu jest w obu przypadkach własna dla hooka: odrzuca wszystko powyżej 4294967295, co niepodpięta funkcja wbudowana przyjmuje i obcina.) Przełączanie jest zablokowane wszędzie tam, gdzie opuszczenie bieżącej ramki byłoby niebezpieczne, a poniższa lista nie jest wyczerpująca:

  • wewnątrz handlera declare(ticks) i wewnątrz obsługi sygnałów pcntl (w obu przypadkach to sprawka samego PHP);
  • podczas budowania wejścia żądania;
  • wewnątrz FILTER_CALLBACK osiągniętego z filter_input_array() z tablicą definicji dla poszczególnych pól (zobacz uwagę pod $_GET w Superglobalne);
  • wewnątrz przejść porządkujących, które uruchamiają destruktory po rozpadzie żądania (po błędzie krytycznym albo po zapisie do żądania, które zostało już anulowane), więc __destruct(), które usypia, by odczekać, robi to, trzymając wątek.

Zwykłe zakończenie żądania do nich nie należy: destruktory i funkcje zamykające działają tam z dozwolonym przełączaniem. oxphp_sleep() i oxphp_usleep() w każdym z tych miejsc również idą ścieżką blokującą, więc sięgnięcie po nie nie jest sposobem na obejście tego.

Fiber uruchomiony przez userlandowy scheduler (AMPHP, Revolt albo gołe new Fiber) to inny przypadek: przełączanie nie jest tam zablokowane, a scheduler normalnie zawiesza i wznawia własne fibery, ale OxPHP nie może wznowić kontekstu, którego nie jest właścicielem, więc hook usuwa się na bok z tego samego powodu co hook streams. Chodzi o fiber uruchomiony wewnątrz żądania; żądanie w trybie worker samo jest fiberem, którym kieruje OxPHP, więc hooki działają na nim normalnie. Kod wewnątrz fibera, który aplikacja uruchomiła sama, śpi na wątku workera i tę część obciążenia trzeba wymiarować tak, jakby hook był wyłączony.

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 albo na jeden ze sposobów dodawanych przez kategorię net (niżej). Kilku popularnych klientów 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 unix://, udp:// i udg://, pary gniazd, strumienie z socket_export_stream() na gniazdach innych niż TCP oraz ssl:///tls:// po aktywacji szyfrowania. Ich tablice operacji są prywatne dla PHP lub dla OpenSSL, więc hook streams do nich nie sięga. Sięga do nich kategoria net: gdy jest wymieniona, odczyt strumieniowy (fread(), fgets() i podobne) na którymkolwiek z nich zawiesza fiber tak jak na tcp://. Bez net trzymają wątek jak dotąd (zanim stream_socket_enable_crypto() się powiedzie, odczyty strumienia SSL są delegowane do zwykłego odczytu gniazda i zawieszają tak czy inaczej). stream_socket_recvfrom() nie jest odczytem strumieniowym: wywołuje bezpośrednio recvfrom(), bez własnego oczekiwania PHP, które dałoby się zawiesić, więc na gnieździe udp:// lub udg:// trzyma wątek, dopóki nie nadejdzie datagram, nawet z net. socket_export_stream() tworzy strumień transportu, którego używa gniazdo: gniazdo UDP, uniksowe lub uniksowe datagramowe staje się strumieniem udp://, unix:// lub udg://, gniazdo TCP zwykłym strumieniem tcp:// (który obejmuje już samo streams), a gniazdo każdego innego rodzaju ogólnym strumieniem, jaki ma para gniazd.

  • Nawiązywanie połączeń i rozwiązywanie DNS. Z net nawiązywanie połączenia przez transporty strumieni PHP zawiesza fiber: stream_socket_client(), fsockopen() i zbudowani na nich klienci. ftp_connect() i socket_connect() łączą się samodzielnie, poza warstwą strumieni, i nie są objęte. Rozwiązywanie DNS również nie zawiesza: nazwa hosta jest rozwiązywana na wątku workera przed rozpoczęciem łączenia, więc wolny resolver go trzyma. Klient z własną biblioteką sieciową (pdo_pgsql i pgsql przez libpq, rozszerzenie memcached przez libmemcached) łączy się i czeka wewnątrz tej biblioteki, której żadna z kategorii nie widzi. Klient trzymający otwarte połączenie zyskuje na każdym zapytaniu niezależnie od tego.

    Wyjątkiem jest trwałe nawiązywanie połączenia, a obejmuje ono więcej niż pfsockopen() i STREAM_CLIENT_PERSISTENT: PDO_MySQL z PDO::ATTR_PERSISTENT, mysqli z przedrostkiem hosta p: i phpredis z pconnect() też otwierają swoje strumienie jako trwałe, a ich nawiązywanie połączenia trzyma wątek przez czas oczekiwania, tak jak przed pojawieniem się net. Powodem jest lista trwałych połączeń. Trwały strumień jest wpisywany do niej pod swoim kluczem, zanim zostanie połączony, więc drugi fiber proszący o ten sam klucz, gdy pierwszy czeka, znalazłby strumień jeszcze bez deskryptora, uznałby go za martwe połączenie i zamknął pod fiberem, który wciąż się łączy. Niektórzy klienci nadają każdemu połączeniu własny klucz (mysqlnd tak robi) i nic nie może w ten sposób znaleźć tych strumieni, ale serwer decyduje na podstawie tego, czy strumień jest trwały, a nie tego, kto mógłby go znaleźć, więc one również trzymają wątek. Kosztem jest nawiązanie połączenia, które połączenie trwałe płaci raz, przy pierwszym otwarciu, a nie odczyty i zapisy na nim.

  • MySQL osiągany przez localhost. Klient MySQL odczytuje localhost jako „użyj gniazda uniksowego", które nie jest strumieniem tcp://, więc hook streams go nie widzi. Zamiast tego wpisz w DSN 127.0.0.1 albo wymień net, która obejmuje strumienie unix://. Dotyczy to tak samo PDO_MySQL i mysqli, i łatwo to przeoczyć, bo wszystko nadal działa, tylko bez korzyści.

  • Zapisy. Przy samym streams 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.)

    Gdy net jest wymieniona, zapis, który musi czekać na miejsce, również zawiesza fiber, a deskryptor jest sprawdzany ponownie po każdym wybudzeniu, więc okno, które w międzyczasie się zamknęło, kosztuje kolejne zawieszenie, a nie wątek. Wyjątkiem jest zwykły strumień z nieskończonym limitem czasu (default_socket_timeout=-1): PHP wysyła na nim blokującym send() i nigdy nie czeka samodzielnie, więc nie ma czego zawiesić, a zapis trzyma wątek tak jak bez hooka. Strumień TLS z nieskończonym limitem czasu to co innego: własna pętla OpenSSL czeka w poll() bez limitu, a to oczekiwanie jest zawieszane.

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.

    Jedno wywołanie PDO jest wyjątkiem, zarówno tutaj, jak i dla kodu działającego poza jakimkolwiek fiberem (niżej): PDO::rollBack() na trwałym połączeniu trzymanym przez inny fiber, gdy zastanie na nim otwartą transakcję, niczego nie wysyła i zwraca false, ponieważ ta transakcja należy do trzymającego, a ROLLBACK wysłany między jego zapytaniami by ją zakończył. Błąd, który zgłasza w sposób określony przez tryb błędów, to błąd już zapisany na połączeniu, który może należeć do trzymającego. Własne niszczenie obiektów PDO też jest przypisane trzymającemu. Dopóki żądanie lub zadanie trzyma trwałe połączenie, porzucenie uchwytu do niego wycofuje otwartą tam transakcję i uruchamia sprzątanie sterownika na koniec żądania tylko wtedy, gdy trzymający był ostatnim, który używał tego uchwytu (zbudował go albo od tego czasu wywołał przez niego jedną z metod PDO chronionych przez OxPHP), bez względu na to, gdzie uchwyt zostaje zniszczony, łącznie z kolektorem cykli PHP działającym w innym żądaniu. Każdy inny uchwyt pomija oba kroki. Na połączeniu, którego nikt nie trzyma, każdy uchwyt wykonuje oba, jak dotąd.

    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. To może kosztować więcej niż samo wywołanie: klient ma prawo odczytać odmówioną operację jako coś gorszego niż nieudane polecenie, a mysqlnd robi tak po obu stronach wymiany. Zapis, który niczego nie wysyła, oznacza połączenie jako utracone i je zamyka, odczyt, który wraca pusty, oznacza je jako utracone tam, gdzie stoi, a każde późniejsze polecenie na nim dostaje odpowiedź z tego stanu, bez dotykania sieci. Traci się całe połączenie, a nie pojedyncze wywołanie, dopóki cokolwiek trzyma uchwyt. To zajmowanie na poziomie klienta opisane wyżej sprawia, że polecenia chronionego klienta w ogóle do tej odmowy nie docierają; protokół napisany ręcznie na surowym gnieździe powinien traktować odmówioną operację każdego rodzaju tak, jak traktuje zamknięte połączenie. Dwa fibery trzymające nawzajem to, na co czeka drugi, rozchodzą się więc na tej granicy, zamiast czekać na siebie w nieskończoność.

    Kod działający poza jakimkolwiek fiberem w ogóle nie może czekać na zajęcie i dotyczy go tylko poziom gniazda. W trybie worker z RUNTIME_HOOKS=streams jest to głównie destruktor, który pętla workera uruchamia między żądaniami, gdy zbiera cykle, podczas gdy inne żądania są zaparkowane. Operacja na gnieździe, którą wykonuje i która musiałaby czekać, na połączeniu, którego trzymający jest na nim zaparkowany w oczekiwaniu na odpowiedź, zawodzi od razu tak, jak zawodzi limit czasu, i niczego nie wysyła; w każdym innym momencie przechodzi. Poziom klienta nie jest tam stosowany, bo jego zajęcie trwa do końca żądania trzymającego: odmawiałby takiemu kodowi połączenia, którego trzymający w danej chwili nie używa, a tak jest przez większość czasu. Koszt odmowy zależy od klienta, jak wyżej. phpredis odrzuca wywołanie, a trzymający zachowuje swoją odpowiedź. mysqlnd albo odmawia polecenia na podstawie własnego stanu, albo, gdy wymiana trzymającego zostawiła ten stan gotowy, dostaje odmowę zapisu i zamyka połączenie, a żądanie trzymającego kończy się tak, jak opisano w punkcie 4. Destruktor jest więc złym miejscem na używanie współdzielonego klienta w ogóle.

  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, a to kosztuje więcej niż samo wywołanie: docierając do gniazda, gdy jakikolwiek fiber jest zaparkowany na połączeniu, dostaje tam odmowę, a dla mysqlnd odmowa oznacza utratę połączenia, jak wyżej, tak samo przy odczycie pobierającym kolejny wiersz niebuforowanego wyniku, jak przy zapisie wysyłającym polecenie. 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.

    Połączenia trwałe są objęte; zobacz Trwałe połączenia PDO poniżej.

  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 przed zamknięciem połączenia. 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 streams 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 Fiber (Fiber::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.

Trwałe połączenia PDO

PDO::ATTR_PERSISTENT trzyma połączenie sterownika w puli współdzielonej przez cały worker, a PDO sięga do tej puli w dwóch krokach, które nie przetrwają przeplecenia: przed połączeniem wyszukuje połączenie w puli, a po nim rejestruje to, co zbudował. Dwa nakładające się konstruktory oba chybiają puli, a druga rejestracja zastępuje pierwszy wpis, zamykając to połączenie pod żądaniem, które akurat czyta na nim swoją odpowiedź. Nakładanie się to zwykły przypadek, bo do static $pdo ??= new PDO(...) dociera każde żądanie, które nadejdzie, zanim pierwsze połączenie się zakończy.

Konstruktory żądające połączenia trwałego do jednego źródła danych działają więc po jednym naraz na worker, a drugi znajduje to, co zarejestrował pierwszy. Źródłem danych jest to, które rozwiązuje PDO (alias z pdo.dsn.* w php.ini liczy się jako DSN, który wskazuje), a konstruktory do różnych źródeł z reguły na siebie nie czekają, więc jeden utknięty na łączeniu z serwerem, który przyjmuje połączenie i nigdy nie odpowiada, nie wstrzymuje konstruktorów do zdrowego. Dwa przypadki czekają więcej, a nie mniej:

  • DSN-y identyczne aż do dwukropka. DSN jest porównywany tylko do pierwszego dwukropka po przedrostku sterownika (host IPv6 to jeden ze sposobów, by go mieć), ponieważ PDO łączy DSN, użytkownika i hasło w klucz puli dwukropkami i niczego nie escapuje, więc DSN rozszerzony o :… może wskazywać ten sam wpis w puli co DSN bez tego rozszerzenia. Źródła identyczne aż do tego dwukropka dzielą jedno oczekiwanie, także utknięte: dwa hosty IPv6 w tej samej początkowej grupie cyfr czekają na siebie nawzajem tak, jak kiedyś czekały wszystkie konstruktory na workerze.
  • DSN-y uri: i DSN-y niebędące łańcuchami. Nie da się ich rozwiązać bez powtarzania tego, co robi z nimi PDO, więc taki konstruktor czeka, aż na jego workerze nie będzie działał żaden konstruktor trwały do jakiegokolwiek źródła danych, a wszystkie czekają, gdy on działa. Czekając, niczego nie trzyma, więc nie wstrzymuje innych, ale gdy działa konstruktor do serwera, który nigdy nie odpowiada, nie może znaleźć takiego momentu, a gdy jego oczekiwanie się wyczerpie, idzie dalej bez serializacji. Opisany wyżej przypadek utkniętego serwera nadal więc dotyczy DSN-ów uri: i obiektowych, a taki, który wskazuje sam utknięty serwer, wstrzymuje każdy konstruktor trwały na swoim workerze tak długo, jak trwa jego łączenie, dokładnie tak jak wcześniej.

To oczekiwanie jest ograniczone tak jak te powyżej, ale jego wyczerpanie nie kończy się błędem zgłaszanym przez klienta: konstruktor, któremu skończy się budżet, idzie dalej bez serializacji i mówi o tym w logu serwera, a dwa konstruktory nakładające się w ten sposób zastępują nawzajem swoje wpisy w puli jak wcześniej. To kończy żądanie czytające odpowiedź na zastąpionym połączeniu, a ponieważ uchwyt z puli jest zwalniany bez względu na to, ile obiektów PDO nadal na niego wskazuje, może zakończyć proces workera, gdy jeden z tych obiektów zostanie później zniszczony. Wewnątrz własnego fibera userlandowego schedulera ten budżet wyczerpuje się od razu: takiego kontekstu nie da się zawiesić, więc konstruktor, który zastanie inny w trakcie działania, od razu rezygnuje.

Drugim krokiem jest kontrola żywotności, którą PDO wykonuje przed wydaniem połączenia z puli: ping na samym połączeniu. Wysłany, gdy trzymający jest zaparkowany w oczekiwaniu na odpowiedź, spotkałby się z odmową, PDO uznałoby połączenie za martwe i otworzyło drugie, a to, co aplikacja zamierzała współdzielić, przestałoby być współdzielone. Połączenie trzymane przez inny fiber jest więc raportowane jako żywe bez pingu, ale tylko konstruktorowi, który zostawiłby je dokładnie takim, jakie jest (tam, gdzie sterownik nie dostarcza kontroli żywotności, jak pdo_sqlite, ta sama odpowiedź zastępuje własną kontrolę PDO). PDO zapisuje tryb błędów, flagę autocommit i pozostałe opcje na wydawanym uchwycie z puli, a na połączeniu w użyciu te zapisy trafiłyby do trzymającego. Połączenie jest więc współdzielone tylko z konstruktorem, który nie zapisałby na nim niczego nowego:

  • każda przekazywana przez niego opcja należy do tych, które PDO przechowuje na samym uchwycie (PDO::ATTR_ERRMODE, PDO::ATTR_CASE, PDO::ATTR_ORACLE_NULLS, PDO::ATTR_DEFAULT_FETCH_MODE; wpis PDO::ATTR_PERSISTENT wskazujący pulę się nie liczy), oraz
  • każda ma wartość, którą połączenie już ma.

Opcja, o której trzeba powiadomić sterownik, jest odrzucana bez względu na wartość, w tym PDO::ATTR_AUTOCOMMIT i PDO::ATTR_STRINGIFY_FETCHES, na każdym sterowniku, także zewnętrznym. Konstruktor niespełniający któregokolwiek warunku słyszy, że połączenie jest martwe, i dostaje własne, a log serwera mówi o tym najwyżej raz na sekundę. Dla klucza puli, którego opcje zawierają PDO::ATTR_EMULATE_PREPARES lub PDO::ATTR_STRINGIFY_FETCHES, jak te Laravela, oznacza to: współdzielone między żądaniami oraz między fiberami, które konstruują przed wykonaniem zapytania, ale konstruktor, który nadejdzie, gdy inne żądanie trzyma połączenie, otwiera własne. Zajęcie trwa od pierwszego zapytania trzymającego do końca jego żądania.

Połączenie, którego nic nie używa, jest sprawdzane jak dotąd, łącznie z takim, którego trzymający porzucił ostatni uchwyt, więc połączenie, które naprawdę umarło, nadal zostaje zastąpione. Ta kontrola jest poleceniem jak każde inne i jest zajmowana jak każde inne: zapytanie przychodzące przez chronionego klienta w trakcie jej trwania czeka na nią, w zwykłej granicy. Przypadki nieobjęte powyżej pozostają nieobjęte także tutaj.

Włączanie i koszt

1, true i all włączają każdą kategorię, łącznie ze streams i net. 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 albo RUNTIME_HOOKS=sleep,streams, aby zachować to, co włączało 0.12.0), 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. Włączenie net łata wpisy tego samego rodzaju w tablicach operacji pozostałych transportów gniazd i OpenSSL, na tych samych warunkach, i sprawia, że biblioteka pomostowa odpowiada na poll() dla wywołań wykonywanych przez własny kod PHP; każdy inny wywołujący, łącznie z samym serwerem, jest przekazywany bezpośrednio do libc.

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 bez żadnej flagi środowiska do ustawienia, a oxphp_sleep() przyjmuje ułamkowe sekundy (oxphp_sleep(0.25)), czyli precyzję, której natywne sleep() (całkowite sekundy) nie potrafi wyrazić. Nie są zwolnione z miejsc, w których fiber nie może się zawiesić: tam trzymają wątek workera dokładnie tak jak podpięte funkcje wbudowane.
  • RUNTIME_HOOKS=sleep włączaj dla kodu, którego nie możesz edytować, na przykład frameworka albo biblioteki zewnętrznej wywołującej natywne sleep()/usleep() bezpośrednio. Jest domyślnie wyłączony, poza fiberem nic nie robi i zachowuje natywny kontrakt wartości zwracanej: podpięte sleep() nadal przyjmuje int, zwraca 0, gdy zawiesza, i to, co zwraca funkcja wbudowana, gdy wraca do niej. Kontrola argumentu jest jego własna, jak wyżej.

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\*. Ustaw na false, a żadna z jego klas nie zostanie zarejestrowana: new OxPHP\Shared\Counter() rzuca Error: Class "OxPHP\Shared\Counter" not found, a /__ox_shared/* i metryki oxphp_shared_* są nieobecne. W kodzie, który musi działać w obu przypadkach, zabezpieczaj się przez class_exists('OxPHP\Shared\Counter'). Nie zabezpieczaj się przez interface_exists('OxPHP\Shared\Shareable'): zwraca prawdę zawsze, bo rozszerzenie rejestruje ten interfejs niezależnie od ustawienia
SHARED_MAX_ENTRIES 100000 Globalny limit liczby żywych obiektów Shared (każdy Counter, Map, Channel, … liczy się jako jeden). Utworzenie obiektu ponad ten limit kończy się CapacityException. Klucze wewnątrz Map się do niego nie wliczają; Map ograniczaj przez maxEntries
SHARED_MAX_BYTES 1073741824 (1 GiB) Globalny limit szacowanej pamięci dla wszystkich obiektów Shared, sprawdzany tylko przy tworzeniu obiektu (CapacityException). Obiekty, które już istnieją, mogą dalej rosnąć ponad niego, więc suma może wskazywać więcej niż limit
SHARED_SOFT_LIMIT_RATIO — Przestarzała i ignorowana; jej ustawienie loguje WARN przy starcie. Nigdy nie miała żadnego efektu: nic nie jest zrzucane, gdy zużycie zbliża się do SHARED_MAX_BYTES / SHARED_MAX_ENTRIES, które po osiągnięciu jedynie odmawiają utworzenia nowego obiektu Shared. Aby dostać wczesne ostrzeżenie, ustaw alert na oxphp_shared_capacity_saturation, publikowane, gdy włączone jest SHARED_METRICS_ENABLED (zobacz Obserwowalność Shared*)
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 — Przestarzała i ignorowana; jej ustawienie loguje WARN przy starcie. Nigdy nie miała żadnego efektu i nie ma globalnej polityki zatruwania, którą mogłaby ustawiać: wyjątek rzucony wewnątrz domknięcia Shared\Mutex nigdy nie zatruwa muteksu, a to, czy nieudana fabryka Shared\Once trwale wyłącza komórkę, wybiera się dla każdej instancji osobno; przekaż Once\FailureMode::Poison do konstruktora (zobacz Shared\Once)
SHARED_LOCK_DIAGNOSTICS warn (strict w kompilacji debug binarki) Międzywątkowy detektor zakleszczeń dla Shared\Mutex: off, warn (loguje i zlicza cykle) lub strict (dodatkowo przerywa je wyjątkiem DeadlockException). Tylko małe litery; każda inna wartość po cichu daje wartość domyślną. Zobacz Detektor zakleszczeń
SHARED_LOCK_POLL_INTERVAL_MS 100 Jak często detektor zakleszczeń skanuje, w milisekundach. Nieużywane przy SHARED_LOCK_DIAGNOSTICS=off
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) Sekret, który muszą nieść jawne wyzwalacze (nagłówek X-OxPHP-Profile, ciasteczko OXPROF, parametr zapytania __oxprof), oraz token bearer dla tras /__profiler/* na serwerze wewnętrznym. Nieustawione lub puste = token nie jest wymagany: przechodzi każda niepusta wartość wyzwalacza, a trasy /__profiler/* odpowiadają bez bearera. Próbkowanie PROFILER_SAMPLE_RATE i funkcje PHP OxPHP\Profile\* nigdy go nie sprawdzają: przy PROFILER_ENABLED=true skrypt wywołujący OxPHP\Profile\start() profiluje własne żądanie bez udziału tokenu
PROFILER_MAX_SPANS 50000 Limit spanów profilu na żądanie. Profile przekraczające ten limit są obcinane
PROFILER_MAX_DEPTH 0 (bez limitu) Maksymalna głębokość zagnieżdżenia rejestrowanych wywołań; głębsze wywołanie i, poza samorekurencją przekraczającą 32 ramki, wszystko, co ono wywołuje, są w profilu nieobecne. Ich czas nie: odczytuje się go jako czas wyłączny wywołania na granicy. Pominięte wywołania nadal wliczają się do PROFILER_MAX_SPANS. Przycinane do 32, czyli głębokości śledzonej przez obserwatora silnika; przy PROFILER_ENABLED=true większa wartość jest przy starcie zgłaszana jako przycięta
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_BROTLI_LEVEL=5 COMPRESSION_ZSTD_LEVEL=6 COMPRESSION_GZIP_LEVEL=6 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
{ "access_log": "all", "async_in_flight_cap": 0, "async_max_fibers": 256, "async_queue_capacity": 0, "async_workers": 0, "brotli_level": 5, "deny_file": {"loaded": false}, "document_root": "/var/www/html/public", "drain_timeout_seconds": 25, "entry_file": "/var/www/html/public/index.php", "executor_type": "sapi", "gzip_level": 6, "header_timeout_seconds": 5, "listen_addr": "0.0.0.0:443", "log_level": "warn", "max_connections": 10000, "max_query_body": 524288, "php_workers": "8", "plugins": { "apm": { "db_capture_params": false, "enabled": true, "hooks_registered": 34, "slow_query_ms": 100 }, "otel": { "enabled": true, "protocol": "grpc", "service_name": "oxphp" } }, "queue_capacity": 1024, "queue_max_waiting": 1024, "queue_max_waiting_bytes": 67108864, "queue_wait_timeout_ms": 1000, "rate_limit": 100, "rate_window_seconds": 60, "runtime_hooks": [], "static_max_age": 2592000, "static_revalidate": false, "superglobals_enabled": true, "tls_enabled": true, "tls_min_version": "1.2", "tokio_workers": 4, "trace_context": true, "trusted_proxies": false, "worker_max_memory_mib": 0, "worker_mode_enabled": false, "zstd_level": 6 }
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.

Prompty dla Twojego agenta

Te prompty zakładają agenta z podłączonym serwerem MCP dokumentacji OxPHP — sam przeczyta strony, których potrzebuje.

prompt
Dostrój OxPHP pod mój projekt. Przeczytaj stronę `operations/configuration` przez serwer MCP dokumentacji OxPHP. Sprawdź, co uruchamia to repozytorium — framework, liczbę workerów, na jaką może sobie pozwolić, kształt żądań, które obsługuje — i podaj mi produkcyjny blok zmiennych środowiskowych z uzasadnieniem każdej wybranej wartości.
prompt
Przejrzyj moją konfigurację OxPHP. Przeczytaj stronę `operations/configuration` przez serwer MCP dokumentacji OxPHP. Porównaj zmienne środowiskowe ustawiane przez to repozytorium z wartościami domyślnymi i powiedz mi, które z moich nadpisań nic nie robią, które są ryzykowne, a które wartości domyślne powinny były zostać nadpisane, a nie zostały.

Zobacz również

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