Limity czasu

OxPHP wymusza dwa niezależne limity czasu, które chronią przed powolnymi klientami i wymykającymi się spod kontroli żądaniami. Limit czasu nagłówków zabezpiecza fazę połączenia na poziomie serwera. Czas wykonywania PHP jest ograniczony przez własną dyrektywę ini max_execution_time PHP (oraz funkcję środowiska uruchomieniowego set_time_limit()). Jedna właściwość tego limitu różni się od PHP-FPM w sposób łatwy do przeoczenia: OxPHP działa na wielowątkowej (ZTS) kompilacji PHP, w której limit mierzy czas zegarowy, a nie czas CPU. Zobacz Czas zegarowy, a nie czas CPU.

Jak to działa

Każde żądanie przechodzi przez następujące fazy:

  1. Połączenie zaakceptowane — rozpoczyna się limit czasu nagłówków. OxPHP czeka, aż klient wyśle kompletny zestaw nagłówków HTTP.
  2. Nagłówki odebrane — limit czasu nagłówków się kończy. Żądanie jest przekazywane do workera PHP.
  3. PHP przetwarza żądanie — kod aplikacji działa pod kontrolą własnego max_execution_time PHP, uzbrajanego jako timer POSIX osobny dla każdego wątku. Po osiągnięciu limitu żądanie zostaje anulowane i wyzwalany jest ujednolicony błąd krytyczny Request cancelled (timeout). W trybie worker każde żądanie ma własny limit, także takie, które worker przyjmuje, gdy obsługuje już inne, a żądanie wstrzymane w oczekiwaniu w chwili wyczerpania limitu jest kończone przy wznowieniu (zobacz Tryb worker → Terminy żądań).
  4. Odpowiedź wysłana — w połączeniach keep-alive cykl powtarza się od kroku 1.
graph TD
  A["TCP connect<br/>(+ TLS handshake if enabled)"] -->|HEADER_TIMEOUT_SECONDS| B["Headers received"]
  B -->|max_execution_time| C["Response sent"]
  C -->|next request, keep-alive| A

W połączeniach keep-alive oba limity czasu obowiązują niezależnie dla każdego żądania w ramach połączenia.

Note

Gdy TLS jest włączony, limit czasu nagłówków rozpoczyna się po zakończeniu uzgadniania TLS, a nie w momencie zaakceptowania połączenia TCP.

Limit czasu nagłówków chroni przed atakami typu slowloris, w których klient wysyła nagłówki po jednym bajcie, aby utrzymywać połączenia otwarte w nieskończoność.

Odmierzanie czasu wykonywania PHP jest w całości delegowane do PHP. Gdy max_execution_time zostanie przekroczony, ujednolicona ścieżka anulowania OxPHP:

  • Ustawia connection_status() & PHP_CONNECTION_TIMEOUT, aby kod użytkownika mógł wykryć przyczynę.
  • Uruchamia wszystkie funkcje zwrotne register_shutdown_function(), dokładnie tak jak robi to PHP-FPM.
  • Zwraca HTTP 504 Gateway Timeout, a komunikat Request cancelled (timeout) zapisuje w dzienniku błędów.

Czas zegarowy, a nie czas CPU

max_execution_time mierzy, jak długo żądanie trwa, a nie ile czasu procesora zużyło. Liczy się każda sekunda oczekiwania: powolne zapytanie do bazy danych, wywołanie HTTP do innej usługi, blokujący fread() na gnieździe, sleep().

To różni się od PHP-FPM, a źródłem różnicy jest kompilacja PHP, nie OxPHP. Kompilacja wielowątkowa (ZTS), której OxPHP wymaga, bo uruchamia wiele interpreterów PHP jako wątki jednego procesu, uzbraja limit jako timer POSIX osobny dla każdego wątku, oparty na zegarze monotonicznym. Kompilacja jednowątkowa (non-thread-safe), na której działa PHP-FPM, uzbraja go przez setitimer(ITIMER_PROF) — timer, który tyka tylko wtedy, gdy proces jest na CPU. Stąd bierze się znana reguła „czekanie na bazę danych nie wlicza się do max_execution_time”. Obowiązuje ona w PHP-FPM, ale nie tutaj.

Skutki wychodzą na jaw przy migracji istniejącej aplikacji:

Żądanie: 25s oczekiwania na zapytanie raportu, 2s renderowania Wliczone do limitu 30s
PHP-FPM (kompilacja jednowątkowa) ~2s (limit jest w praktyce nieosiągalny)
OxPHP (kompilacja wielowątkowa) 27s (jedno powolne zapytanie może wyczerpać limit)
Migracja z PHP-FPM

Aplikacja, która pod PHP-FPM ani razu nie przekroczyła max_execution_time, po przejściu na OxPHP może zacząć zwracać 504 na najwolniejszych endpointach, mimo że wartość dyrektywy się nie zmieniła. Dobieraj limit do całkowitego opóźnienia żądania, wliczając wszystko, na co żądanie czeka, i zwiększaj go dla endpointów, które blokują się na systemach zewnętrznych.

Kody statusu anulowania

OxPHP anuluje żądanie z kilku różnych powodów. Każdy z nich jest mapowany na status przesyłany po sieci, który odzwierciedla rzeczywisty stan, zamiast ogólnego 500:

Przyczyna Status Uwagi
Przekroczono max_execution_time / set_time_limit() 504 Gateway Timeout Wyczerpanie czasu wykonywania po stronie serwera.
Łagodne zamknięcie serwera przerwało obsługę żądania 503 Service Unavailable Dodaje Retry-After: 5, aby klienci ponawiali próbę wobec przywróconej lub zastępczej instancji.
Klient zamknął połączenie w trakcie żądania 499 „Client Closed Request” w stylu nginx. Połączenie już nie istnieje, więc ten status pojawia się wyłącznie w dziennikach dostępu i metrykach — nigdy nie jest przesyłany po sieci. Ujawnia przerwania inicjowane przez klienta jako status inny niż 5xx, dzięki czemu nie zaśmiecają one alertów o błędach serwera.

To jedyne trzy przyczyny anulowania. Żądanie, które według obserwacji nadzorcy działa dłużej niż jego próg, jest zgłaszane przez oxphp_worker_stuck_total i oxphp_worker_long_running_total i dalej działa; serwer nigdy go nie anuluje.

Jeśli twój ERROR_PAGES_DIR zawiera tylko 500.html, dodaj 504.html, 503.html oraz (opcjonalnie) 499.html, aby ostylowane strony były spójne dla wszystkich przyczyn anulowania.

Konfiguracja

Zmienna Domyślnie Opis
HEADER_TIMEOUT_SECONDS 5 Maksymalna liczba sekund na odebranie nagłówków żądania po zaakceptowaniu połączenia. Chroni przed atakami slowloris. 0 nie jest traktowane w szczególny sposób — jest przekazywane do hyper jako zerowy limit czasu, który wyzwala się natychmiast. Aby wyłączyć limit czasu, usuń zmienną, zamiast ustawiać ją na 0

Czas wykonywania PHP konfiguruje się poprzez php.ini, a nie zmienne środowiskowe OxPHP:

php.ini
; php.ini max_execution_time = 30

Albo dla poszczególnego skryptu w czasie działania:

php
set_time_limit(60); // 60 seconds from now set_time_limit(0); // disable for this request

Zalecane wartości

Scenariusz Limit czasu nagłówków max_execution_time
Serwer API 5s 30s
Ogólne serwowanie stron WWW 5s 60s
Przesyłanie plików 10s 300s
SSE / long-polling 5s 0 (wyłączone, ustawiane dla poszczególnego skryptu)

Dostosuj te wartości do charakterystyki swojej aplikacji. W przypadku endpointów SSE wywołaj set_time_limit(0) na początku skryptu strumieniującego, zamiast globalnie wyłączać max_execution_time.

Rozwiązywanie problemów

Klienci nieoczekiwanie otrzymują 504 z komunikatem „Request cancelled (timeout)”

Limit czasu wykonywania PHP zadziałał, zanim skrypt zdążył się zakończyć.

Jeśli te żądania działały bez problemu pod PHP-FPM, a zaczęły kończyć się błędem po przejściu na OxPHP, przyczyną jest najpewniej semantyka timera opisana w sekcji Czas zegarowy, a nie czas CPU: czas, jaki żądanie spędza na oczekiwaniu na bazę danych lub usługę nadrzędną, wlicza się do limitu.

Rozwiązanie: Zwiększ max_execution_time dla danego skryptu albo wywołaj set_time_limit($seconds), aby wydłużyć go w czasie działania:

php
// at the top of a slow script set_time_limit(300);

W przypadku endpointów SSE lub strumieniujących, gdzie połączenia muszą pozostawać otwarte w nieskończoność, wyłącz licznik dla tego skryptu:

php
set_time_limit(0);
Połączenia są zrywane, zanim dotrą nagłówki

Limit czasu nagłówków jest zbyt krótki dla klientów na łączach o wysokim opóźnieniu lub za powolnymi load balancerami.

Rozwiązanie: Zwiększ limit czasu nagłówków:

bash
HEADER_TIMEOUT_SECONDS=15
OPcache powoduje przekroczenie limitu czasu pierwszego żądania po zmianie skryptu

Rekompilacja OPcache zwiększa opóźnienie pierwszego żądania po zmianie pliku. Zdarza się to częściej w środowiskach deweloperskich z wieloma plikami. Zwiększ max_execution_time lub wyłącz go na czas prac deweloperskich.

Przykład dla Dockera

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "8080:80" environment: HEADER_TIMEOUT_SECONDS: "5" volumes: - ./app:/var/www/html:ro - ./php.ini:/usr/local/etc/php/conf.d/zz-app.ini:ro

Z plikiem php.ini zawierającym:

php.ini
max_execution_time = 30

Dobre praktyki

  • Nigdy nie ustawiaj max_execution_time = 0 globalnie w środowisku produkcyjnym, chyba że masz endpointy SSE lub long-polling, które wymagają nieskończenie otwartych połączeń. Preferuj set_time_limit(0) dla poszczególnych skryptów.
  • Stosuj krótsze limity dla serwerów API. API mają przewidywalne czasy odpowiedzi. 30-sekundowy max_execution_time szybko wychwytuje zablokowane żądania, nie wpływając na normalny ruch. Dobieraj go do najwolniejszej zależności, na którą czekasz, a nie do pracy CPU wykonywanej przez skrypt, bo czas oczekiwania też się liczy.
  • Łącz z ograniczaniem liczby żądań. Limity czasu chronią pojedyncze żądania; ograniczanie liczby żądań chroni przed dużym wolumenem żądań. Razem obejmują zarówno powolne, jak i szybkie wzorce ataków.

Prompty dla Twojego agenta

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

prompt
Ustaw limity czasu OxPHP wokół mojego najdłuższego żądania. Przeczytaj stronę `features/timeouts` przez serwer MCP dokumentacji OxPHP. Znajdź w tym repozytorium to, co faktycznie działa najdłużej (przesyłanie plików, raporty, eksporty oraz oczekiwanie na bazy danych i usługi nadrzędne, bo limit liczy czas zegarowy), a potem podaj mi limit czasu nagłówków i `max_execution_time`, z jakimi mam działać, oraz nadpisania per skrypt dla wszystkiego, co nie zmieści się pod globalnym limitem.

Zobacz też

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