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:
- Połączenie zaakceptowane — rozpoczyna się limit czasu nagłówków. OxPHP czeka, aż klient wyśle kompletny zestaw nagłówków HTTP.
- Nagłówki odebrane — limit czasu nagłówków się kończy. Żądanie jest przekazywane do workera PHP.
- PHP przetwarza żądanie — kod aplikacji działa pod kontrolą własnego
max_execution_timePHP, uzbrajanego jako timer POSIX osobny dla każdego wątku. Po osiągnięciu limitu żądanie zostaje anulowane i wyzwalany jest ujednolicony błąd krytycznyRequest 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ń). - 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.
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 komunikatRequest 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) |
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
max_execution_time = 30Albo dla poszczególnego skryptu w czasie działania:
set_time_limit(60); // 60 seconds from now
set_time_limit(0); // disable for this requestZalecane 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:
// 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:
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:
HEADER_TIMEOUT_SECONDS=15OPcache 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
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:roZ plikiem php.ini zawierającym:
max_execution_time = 30Dobre praktyki
- Nigdy nie ustawiaj
max_execution_time = 0globalnie w środowisku produkcyjnym, chyba że masz endpointy SSE lub long-polling, które wymagają nieskończenie otwartych połączeń. Preferujset_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_timeszybko 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.
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ż
- Ograniczanie liczby żądań — ograniczanie liczby żądań na adres IP
- Server-Sent Events (SSE) — wskazówki dotyczące wyłączania limitów czasu dla endpointów strumieniujących
- Dokumentacja konfiguracji — pełna dokumentacja zmiennych środowiskowych