Коректне завершення роботи

OxPHP обробляє SIGTERM і SIGINT так, щоб запити, які вже виконуються, встигли завершитися до виходу процесу. Це важливо для розгортань без простою та поступових оновлень (rolling updates) в оркестраторах контейнерів.

Обробка сигналів

OxPHP реагує на два сигнали завершення:

Сигнал Джерело Поведінка
SIGTERM Оркестратори контейнерів, docker stop, kill Запускає коректне завершення
SIGINT Ctrl+C у терміналі Запускає коректне завершення

Обидва сигнали запускають ту саму послідовність завершення. Достатньо першого сигналу: сервер одразу починає злив (drain).

Послідовність завершення

Отримавши сигнал завершення, OxPHP виконує таку послідовність:

  1. Припинення прийому нових зʼєднань: сервер перестає приймати зʼєднання на основному порту, але сам прослуховувальний сокет лишається відкритим до виходу процесу. Тому клієнт, який підключається під час зливу, завершує TCP-рукостискання й так і не отримує відповіді: він чекає до власного тайм-ауту або доки процес не завершиться й не скине зʼєднання. Відхиляються одразу лише ті зʼєднання, які встановлюються вже після виходу, тож виводьте екземпляр з ротації ще до того, як до нього дійде сигнал (див. Kubernetes). PHP-воркери продовжують працювати, щоб обробити запити, які вже виконуються.
  2. Згортання активних зʼєднань — клієнти HTTP/2 отримують фрейм GOAWAY, а простійні keep-alive зʼєднання HTTP/1.1 закриваються, тож клієнти переходять на справний екземпляр замість того, щоб мультиплексувати нові запити в той, що зупиняється. Відкриті потоки завершуються швидко й коректно — це стосується будь-якої відповіді, яка вже почала надсилати chunked-вивід, як скінченних завантажень, так і SSE, — див. Server-Sent Events.
  3. Злив запитів, що виконуються — звичайним активним запитам дають спокійно завершитися з повними відповідями. Сервер перевіряє їх завершення кожні 100 мс. Внутрішній сервер health/metrics лишається доступним увесь час зливу, тож readiness-проби продовжують працювати. Робота, яку запит продовжує виконувати після oxphp_finish_request(), теж вважається такою, що виконується, хоча його зʼєднання вже закрито.
  4. Застосування дедлайну зливу: запити, які досі виконуються після DRAIN_TIMEOUT_SECONDS, скасовуються (їхні колбеки register_shutdown_function() все одно виконуються) і отримують ~2 секунди на згортання, перш ніж сервер піде далі. Запит, що перебуває всередині блокувального виклику (sleep(), запит до бази даних, читання із сокета), не можна там перервати. Це завжди так у традиційному режимі, а в режимі воркерів — якщо лише RUNTIME_HOOKS не перетворив цей виклик на призупинення файбера. Його зʼєднання закривається за розкладом, але його потік воркера, а разом із ним і процес, завершується лише після повернення з виклику. Отже, ці ~2 секунди не обмежують момент виходу процесу.
  5. Скидання буферів плагінів — записи журналу доступу та APM-спани, накопичені під час вікна зливу, скидаються.
  6. Зупинка асинхронного пулу — фоновий пул асинхронних задач зупиняється.
  7. Зупинка внутрішнього сервера — сервер health/metrics зупиняється після завершення зливу.
  8. Вихід — процес завершується з кодом 0.
Note

Кроки 1 і 2 починаються в момент надходження сигналу; відлік дедлайну зливу починається лише після зупинки циклу прийому зʼєднань. Якщо на момент сигналу зайняті всі зʼєднання, дозволені MAX_CONNECTIONS, цикл чекає, доки одне з них закриється, і лише тоді зупиняється. Доки не закриється хоча б одне зʼєднання, дедлайн не відраховується, і на кроці 4 нічого з того, що ще виконується, не скасовується. Якщо ліміт тримають простійні keep-alive зʼєднання, крок 2 закриває їх, і це минає за лічені миті; якщо ж його тримають довгі запити або повільні клієнти, це триває стільки, скільки й вони, і може вичерпати весь період очікування (grace period) оркестратора.

Note

Потоки PHP-воркерів не зупиняються на початку послідовності. Їм надсилається сигнал на вихід, і їх приєднують (join) у самому кінці, коли злив завершився і процес уже виходить. Саме тому злив має враховувати роботу, що досі виконується на воркері, навіть коли клієнтських зʼєднань уже не лишилося.

Налаштування

Змінна За замовчуванням Опис
DRAIN_TIMEOUT_SECONDS 25 Максимальна кількість секунд, яку отримують запити, що виконуються, на завершення до скасування; сервер припиняє обслуговування приблизно через 2 секунди після дедлайну, і процес тоді ж завершується, якщо лише запит не перебуває всередині блокувального виклику (див. крок 4 вище). Значення за замовчуванням залишає запас для згортання після дедлайну та скидання телеметрії в межах стандартного 30-секундного періоду очікування завершення в Kubernetes

Встановіть DRAIN_TIMEOUT_SECONDS так, щоб вистачило на ваш найповільніший очікуваний запит:

  • API-сервери зі швидкими відповідями: 10–15 секунд
  • Застосунки із завантаженням файлів або довгими запитами до бази: 30–60 секунд
  • Режим воркерів із фоновою обробкою: відповідно до вашої найдовшої очікуваної операції

Kubernetes

У Kubernetes під час поступового оновлення завершення відбувається так:

  1. Kubernetes надсилає поду SIGTERM.
  2. Под вилучається зі списку ендпоінтів Service.
  3. OxPHP зливає зʼєднання, що обробляються, протягом DRAIN_TIMEOUT_SECONDS, потім скасовує запити, що затрималися, і припиняє обслуговування приблизно через 2 секунди. Тоді ж процес і завершується — або коли повернеться останній блокувальний виклик на воркері.
  4. Якщо под усе ще працює після terminationGracePeriodSeconds, Kubernetes надсилає SIGKILL.

Встановіть terminationGracePeriodSeconds більшим за DRAIN_TIMEOUT_SECONDS + 2, додавши найдовший блокувальний виклик, у якому запит може перебувати в момент дедлайну (найдовший запит до бази або тайм-аут сокета, який дозволяє застосунок), щоб злив, разом зі згортанням після дедлайну та скиданням телеметрії, завершився до примусового знищення. Якщо додати лише + 2, то запит, який на момент дедлайну виконує довгий запит до бази, призведе до того, що под буде вбито SIGKILL (код виходу 137) замість виходу з кодом 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"

Хук pre-stop

Якщо ваш сервіс отримує трафік від зовнішніх балансувальників навантаження, які повільно поширюють зміни ендпоінтів, додайте хук pre-stop, щоб відкласти послідовність завершення:

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

Так балансувальник встигне вилучити под зі свого списку цілей, перш ніж OxPHP припинить приймати зʼєднання. Це важливо, бо зʼєднання, спрямоване на под після сигналу, лишається без відповіді до кінця зливу, а не відхиляється (див. крок 1 послідовності завершення).

Docker

Docker надсилає SIGTERM, коли ви виконуєте docker stop. Стандартний тайм-аут зупинки Docker — 10 секунд, після чого Docker надсилає SIGKILL.

Щоб OxPHP мав досить часу на злив, збільште тайм-аут зупинки:

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

Або задайте його у файлі Compose:

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

Повідомлення журналу

Під час коректного завершення OxPHP виводить структуровані повідомлення журналу, які можна відстежувати:

Успішний злив:

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"}

Ці два лічильники перетинаються, а не ділять запити між собою: active_connections — це активні клієнтські зʼєднання, in_flight_requests — запити, які ще виконуються на PHP-воркерах, а запит, що тримає зʼєднання, враховується в обох. Запит, який завершив свою відповідь раніше за допомогою oxphp_finish_request(), враховується лише в in_flight_requests — його зʼєднання вже закрито, а фонова робота триває.

Досягнуто дедлайну зливу:

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"}

Якщо ви регулярно бачите попередження "Drain timeout reached", збільште DRAIN_TIMEOUT_SECONDS або дослідіть довгі запити за допомогою гістограми oxphp_request_duration_us.

Промпти для вашого агента

Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.

prompt
Зроби так, щоб моє розгортання OxPHP зупинялося, не втрачаючи запитів. Прочитай сторінку `operations/graceful-shutdown` через MCP-сервер документації OxPHP. Узгоджено між собою налаштуй у моїх маніфестах Kubernetes період очікування завершення, хук preStop і тайм-аути завершення, врахувавши найдовший блокувальний виклик (тайм-аут запиту до бази або сокета), який дозволяє мій застосунок, і поясни, що відбувається із запитом, який виконується, на кожному кроці.

Дивіться також