Łagodne zamknięcie

OxPHP obsługuje SIGTERM i SIGINT tak, aby będące w trakcie realizacji żądania zdążyły się zakończyć, zanim proces się zamknie. Ma to znaczenie dla wdrożeń bez przestojów oraz aktualizacji kroczących (rolling updates) w orkiestracji kontenerów.

Obsługa sygnałów

OxPHP reaguje na dwa sygnały zamknięcia:

Sygnał Źródło Zachowanie
SIGTERM Orkiestratory kontenerów, docker stop, kill Rozpoczyna łagodne zamknięcie
SIGINT Ctrl+C w terminalu Rozpoczyna łagodne zamknięcie

Oba sygnały uruchamiają tę samą sekwencję zamknięcia. Wystarczy tylko pierwszy sygnał. Serwer natychmiast rozpoczyna wygaszanie.

Sekwencja zamknięcia

Po otrzymaniu sygnału zamknięcia OxPHP wykonuje następującą sekwencję:

  1. Zaprzestanie przyjmowania nowych połączeń: serwer przestaje przyjmować połączenia na głównym porcie, ale samo gniazdo nasłuchujące pozostaje otwarte aż do zakończenia procesu. Klient, który połączy się w trakcie wygaszania, kończy więc uzgadnianie TCP (handshake) i nigdy nie dostaje odpowiedzi: czeka do upływu własnego limitu czasu albo do chwili, gdy proces się zakończy i zresetuje połączenie. Wprost odrzucane są dopiero połączenia nawiązane po zakończeniu procesu, dlatego wycofaj instancję z rotacji, zanim dotrze do niej sygnał (zobacz Kubernetes). Workery PHP nadal działają, przetwarzając żądania będące w trakcie realizacji.
  2. Wygaszanie aktywnych połączeń — klienci HTTP/2 otrzymują ramkę GOAWAY, a bezczynne połączenia keep-alive HTTP/1.1 zostają zamknięte, dzięki czemu klienci przechodzą na sprawną instancję, zamiast multipleksować nowe żądania do zamierającej. Otwarte strumienie kończone są sprawnie i czysto — liczy się każda odpowiedź, która zaczęła wysyłać dane w trybie chunked, zarówno skończone pobrania, jak i SSE — zobacz Server-Sent Events.
  3. Wygaszanie żądań w trakcie realizacji — zwykłe aktywne żądania są pozostawione, aby dokończyć się z pełnymi odpowiedziami. Serwer sprawdza ich zakończenie co 100 ms. Wewnętrzny serwer stanu/metryk pozostaje dostępny przez cały czas wygaszania, dzięki czemu sondy gotowości (readiness) nadal działają. Praca, którą żądanie kontynuuje po oxphp_finish_request(), również liczy się jako będąca w toku, mimo że jego połączenie już nie istnieje.
  4. Egzekwowanie terminu wygaszania: żądania nadal działające po upływie DRAIN_TIMEOUT_SECONDS zostają anulowane (ich funkcje wywoływane zwrotnie przez register_shutdown_function() i tak są uruchamiane) i otrzymują ~2 sekundy na uporządkowanie, zanim serwer przejdzie dalej. Żądania znajdującego się wewnątrz wywołania blokującego (sleep(), zapytanie do bazy danych, odczyt z gniazda) nie da się w tym miejscu przerwać. Dotyczy to zawsze trybu tradycyjnego, a w trybie worker wtedy, gdy RUNTIME_HOOKS nie zamieniło tego wywołania w zawieszenie fibera. Jego połączenie zostaje zamknięte zgodnie z harmonogramem, ale jego wątek workera, a wraz z nim proces, kończy działanie dopiero, gdy wywołanie powróci. Owe ~2 sekundy nie wyznaczają więc górnej granicy momentu zakończenia procesu.
  5. Opróżnienie buforów wtyczek — wpisy rejestru dostępu oraz spany APM zbuforowane w oknie wygaszania zostają zapisane.
  6. Zamknięcie puli asynchronicznej — pula asynchronicznych zadań w tle zostaje zatrzymana.
  7. Zatrzymanie serwera wewnętrznego — serwer stanu/metryk zostaje zatrzymany po zakończeniu wygaszania.
  8. Wyjście — proces kończy działanie z kodem statusu 0.
Note

Kroki 1 i 2 rozpoczynają się w chwili nadejścia sygnału; termin wygaszania zaczyna biec dopiero wtedy, gdy pętla akceptowania połączeń się zatrzyma. Jeśli w chwili nadejścia sygnału w użyciu są wszystkie połączenia dozwolone przez MAX_CONNECTIONS, pętla czeka na zamknięcie któregoś z nich i dopiero wtedy się zatrzymuje. Dopóki żadne połączenie się nie zamknie, żaden termin nie biegnie i nic, co wciąż działa, nie zostanie anulowane w kroku 4. Jeśli limit zajmują bezczynne połączenia keep-alive, krok 2 je zamyka i ten stan mija w ciągu chwili; jeśli zajmują go długie żądania lub wolni klienci, trwa tak długo jak one i może pochłonąć cały okres karencji orkiestratora.

Note

Wątki workerów PHP nie są zatrzymywane na początku sekwencji. Sygnał do zakończenia i dołączenie (join) otrzymują na samym końcu, gdy wygaszanie już się zakończyło i proces jest w drodze do zamknięcia. Dlatego wygaszanie musi uwzględniać pracę wciąż wykonywaną na workerze, nawet gdy nie zostało żadne połączenie klienta.

Konfiguracja

Zmienna Wartość domyślna Opis
DRAIN_TIMEOUT_SECONDS 25 Maksymalna liczba sekund, jaką będące w trakcie realizacji żądania dostają na zakończenie, zanim zostaną anulowane; serwer przestaje obsługiwać ruch około 2 sekund po upływie terminu, a proces kończy wtedy działanie, chyba że jakieś żądanie znajduje się wewnątrz wywołania blokującego (zobacz krok 4 powyżej). Wartość domyślna pozostawia zapas na porządkowanie po terminie i opróżnienie telemetrii w ramach domyślnego 30-sekundowego okresu karencji na zakończenie (termination grace period) w Kubernetes

Ustaw DRAIN_TIMEOUT_SECONDS tak, aby uwzględniał najwolniejsze spodziewane żądanie:

  • Serwery API z szybkimi odpowiedziami: 10–15 sekund
  • Aplikacje z przesyłaniem plików lub długimi zapytaniami: 30–60 sekund
  • Tryb worker z przetwarzaniem w tle: dopasuj do najdłuższej spodziewanej operacji

Kubernetes

W Kubernetes przebieg zamknięcia podczas aktualizacji kroczącej wygląda następująco:

  1. Kubernetes wysyła SIGTERM do poda.
  2. Pod zostaje usunięty z listy endpointów Service.
  3. OxPHP wygasza połączenia będące w trakcie realizacji w czasie DRAIN_TIMEOUT_SECONDS, następnie anuluje maruderów i przestaje obsługiwać ruch około 2 sekund później. Proces kończy wtedy działanie albo dopiero wtedy, gdy powróci ostatnie wywołanie blokujące na workerze.
  4. Jeśli pod nadal działa po upływie terminationGracePeriodSeconds, Kubernetes wysyła SIGKILL.

Ustaw terminationGracePeriodSeconds powyżej wartości DRAIN_TIMEOUT_SECONDS + 2, powiększonej o najdłuższe wywołanie blokujące, w którym żądanie może się znajdować w chwili upływu terminu (najdłuższe zapytanie lub limit czasu gniazda, na jaki pozwala aplikacja), aby wygaszanie, łącznie z porządkowaniem po terminie i opróżnieniem telemetrii, zdążyło się zakończyć przed wymuszonym zabiciem procesu. Przy samym + 2 żądanie tkwiące w długim zapytaniu w chwili upływu terminu sprawi, że pod zostanie zabity sygnałem SIGKILL (kod wyjścia 137), zamiast zakończyć działanie z kodem 0:

yaml
apiVersion: apps/v1 kind: Deployment spec: template: spec: terminationGracePeriodSeconds: 45 containers: - name: oxphp image: ghcr.io/oxphp/oxphp:0.13.0 env: - name: DRAIN_TIMEOUT_SECONDS value: "30"

Hak pre-stop

Jeśli twoja usługa odbiera ruch z zewnętrznych load balancerów, które wolno propagują zmiany endpointów, dodaj hak pre-stop, aby opóźnić sekwencję zamknięcia:

yaml
lifecycle: preStop: exec: command: ["sleep", "5"]

Daje to load balancerowi czas na usunięcie poda ze swojej listy celów, zanim OxPHP przestanie przyjmować połączenia. Ma to znaczenie, ponieważ połączenie skierowane do poda po nadejściu sygnału pozostaje bez odpowiedzi przez resztę wygaszania, zamiast zostać odrzucone (zobacz krok 1 sekwencji zamknięcia).

Docker

Docker wysyła SIGTERM, gdy uruchamiasz docker stop. Domyślny limit czasu zatrzymania w Docker wynosi 10 sekund, po którego upływie Docker wysyła SIGKILL.

Aby dać OxPHP wystarczająco dużo czasu na wygaszenie, zwiększ limit czasu zatrzymania:

bash
docker stop --time 45 my-oxphp-container

Albo ustaw go w pliku Compose:

compose.yaml
services: oxphp: image: ghcr.io/oxphp/oxphp:0.13.0 stop_grace_period: 45s environment: DRAIN_TIMEOUT_SECONDS: "30"

Komunikaty w logach

Podczas łagodnego zamknięcia OxPHP emituje ustrukturyzowane komunikaty w logach, które możesz monitorować:

Udane wygaszenie:

json
{"level":"INFO","message":"Received shutdown signal, draining connections"} {"level":"INFO","message":"Draining in-flight connections","active_connections":3,"in_flight_requests":4} {"level":"INFO","message":"All connections drained"} {"level":"INFO","message":"Server stopped"}

Obie liczby nakładają się na siebie, a nie dzielą zbiór na części: active_connections to aktywne połączenia klientów, in_flight_requests to żądania wciąż wykonywane na workerach PHP, a żądanie utrzymujące połączenie jest liczone w obu. Żądanie, które zakończyło swoją odpowiedź wcześniej przez oxphp_finish_request(), pojawia się tylko w in_flight_requests — jego połączenie już nie istnieje, podczas gdy jego praca w tle trwa.

Osiągnięto termin wygaszania:

json
{"level":"INFO","message":"Received shutdown signal, draining connections"} {"level":"WARN","message":"Drain timeout reached, cancelling in-flight requests","remaining_connections":1,"in_flight_requests":1} {"level":"INFO","message":"All connections drained"} {"level":"INFO","message":"Server stopped"}

Jeśli regularnie widzisz ostrzeżenie „Drain timeout reached", zwiększ DRAIN_TIMEOUT_SECONDS lub zbadaj długo działające żądania za pomocą histogramu oxphp_request_duration_us.

Prompty dla Twojego agenta

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

prompt
Spraw, by moje wdrożenie OxPHP zamykało się bez gubienia żądań. Przeczytaj stronę `operations/graceful-shutdown` przez serwer MCP dokumentacji OxPHP. Ustaw okres karencji na zakończenie, hook preStop i limity czasu zamykania w moich manifestach Kubernetes tak, aby były ze sobą spójne, uwzględniając najdłuższe wywołanie blokujące (zapytanie lub limit czasu gniazda), na jakie pozwala moja aplikacja, i powiedz mi, co dzieje się z żądaniem w toku na każdym kroku.

Zobacz też

  • Kontrole stanu — sondy gotowości i interakcja z zamknięciem
  • Dokumentacja konfiguracji — wszystkie zmienne środowiskowe, w tym DRAIN_TIMEOUT_SECONDS
  • Metryki — oxphp_active_connections śledzi połączenia podczas wygaszania
Znalazłeś błąd? Zgłoś go →