Корректное завершение работы
OxPHP обрабатывает SIGTERM и SIGINT, чтобы выполняющиеся запросы успевали завершиться до выхода из процесса. Это важно для развёртываний без простоя и плавающих обновлений (rolling updates) в системах оркестрации контейнеров.
Обработка сигналов
OxPHP реагирует на два сигнала завершения:
| Сигнал | Источник | Поведение |
|---|---|---|
SIGTERM |
Оркестраторы контейнеров, docker stop, kill |
Запускает корректное завершение работы |
SIGINT |
Ctrl+C в терминале | Запускает корректное завершение работы |
Оба сигнала запускают одну и ту же последовательность завершения. Достаточно только первого сигнала. Сервер немедленно начинает слив соединений.
Последовательность завершения
При получении сигнала завершения OxPHP выполняет следующую последовательность:
- Прекращение приёма новых соединений: сервер перестаёт принимать соединения на основном порту, но сам слушающий сокет остаётся открытым до выхода процесса. Поэтому клиент, подключившийся во время слива, проходит TCP-рукопожатие, но ответа так и не получает: он ждёт до собственного таймаута или до тех пор, пока процесс не завершится и не сбросит соединение. Сразу отклоняются только соединения, установленные уже после выхода, поэтому выводите экземпляр из ротации до того, как до него дойдёт сигнал (см. Kubernetes). PHP-воркеры продолжают работать, обрабатывая выполняющиеся запросы.
- Сворачивание активных соединений — HTTP/2-клиенты получают кадр
GOAWAY, а простаивающие keep-alive-соединения HTTP/1.1 закрываются, чтобы клиенты переходили на исправный экземпляр, а не мультиплексировали новые запросы в завершающийся. Открытые потоки завершаются быстро и аккуратно — сюда относится любой ответ, который уже начал сбрасывать вывод по частям (chunked), — как конечные загрузки, так и SSE — см. Server-Sent Events. - Слив выполняющихся запросов — обычные активные запросы не прерываются и завершаются с полными ответами. Сервер проверяет их завершение каждые 100 мс. Внутренний сервер проверок работоспособности и метрик остаётся доступным на протяжении всего слива, поэтому проверки готовности (readiness probes) продолжают работать. Работа, которую запрос продолжает выполнять после
oxphp_finish_request(), тоже считается выполняющейся, хотя его соединения уже нет. - Соблюдение крайнего срока слива — запросы, всё ещё выполняющиеся по истечении
DRAIN_TIMEOUT_SECONDS, отменяются (их коллбэкиregister_shutdown_function()при этом всё равно выполняются) и получают ~2 секунды на завершение, прежде чем сервер продолжит. Запрос, находящийся внутри блокирующего вызова (sleep(), запрос к базе данных, чтение из сокета), в этом месте прервать нельзя. В традиционном режиме это верно всегда, а в режиме воркеров — если толькоRUNTIME_HOOKSне превратил этот вызов в приостановку файбера. Его соединение закрывается по расписанию, но его поток воркера, а вместе с ним и процесс, завершается только после возврата из вызова. Поэтому ~2 секунды не ограничивают момент выхода процесса. - Сброс плагинов — записи access-log и APM-спаны, накопленные в буфере во время окна слива, сбрасываются.
- Остановка асинхронного пула — фоновый пул асинхронных задач останавливается.
- Остановка внутреннего сервера — сервер проверок работоспособности и метрик останавливается после завершения слива.
- Выход — процесс завершается с кодом состояния 0.
Шаги 1 и 2 начинаются в момент поступления сигнала; отсчёт крайнего срока слива начинается только после остановки цикла приёма соединений. Если в момент поступления сигнала заняты все соединения, допускаемые MAX_CONNECTIONS, цикл ждёт закрытия одного из них и останавливается только тогда. Пока какое-либо соединение не закроется, крайний срок не отсчитывается и ничто из выполняющегося не отменяется на шаге 4. Если лимит занят простаивающими keep-alive-соединениями, шаг 2 их закрывает, и эта фаза проходит за считанные мгновения; если его занимают долгие запросы или медленные клиенты, она длится столько же, сколько они, и может исчерпать весь период отсрочки оркестратора.
Потоки PHP-воркеров не останавливаются в начале последовательности. Сигнал на завершение они получают — и присоединяются — в самом конце, когда слив закончен и процесс уже на пути к выходу. Именно поэтому слив должен учитывать работу, всё ещё выполняющуюся на воркере, даже когда клиентского соединения уже не осталось.
Конфигурация
| Переменная | По умолчанию | Описание |
|---|---|---|
DRAIN_TIMEOUT_SECONDS |
25 |
Максимальное число секунд, которое выполняющиеся запросы получают на завершение, прежде чем будут отменены; сервер прекращает обслуживание примерно через 2 секунды после крайнего срока, и процесс тогда же завершается, если только какой-либо запрос не находится внутри блокирующего вызова (см. шаг 4 выше). Значение по умолчанию оставляет запас для завершения после крайнего срока и сброса телеметрии в рамках стандартного 30-секундного периода отсрочки завершения (termination grace period) в Kubernetes |
Задавайте DRAIN_TIMEOUT_SECONDS с учётом самого медленного ожидаемого запроса:
- API-серверы с быстрыми ответами:
10–15секунд - Приложения с загрузкой файлов или долгими запросами:
30–60секунд - Режим воркеров с фоновой обработкой: под самую длинную ожидаемую операцию
Kubernetes
В Kubernetes процесс завершения при плавающем обновлении (rolling update) выглядит так:
- Kubernetes отправляет
SIGTERMв под. - Под удаляется из списка эндпоинтов Service.
- OxPHP сливает выполняющиеся соединения в пределах
DRAIN_TIMEOUT_SECONDS, затем отменяет отставших и примерно через 2 секунды прекращает обслуживание. Процесс завершается тогда же или после возврата из последнего блокирующего вызова на воркере. - Если под всё ещё работает по истечении
terminationGracePeriodSeconds, Kubernetes отправляетSIGKILL.
Задавайте terminationGracePeriodSeconds больше, чем DRAIN_TIMEOUT_SECONDS + 2 плюс самый долгий блокирующий вызов, в котором запрос может находиться в момент истечения крайнего срока (самый долгий запрос к базе данных или таймаут сокета, допускаемый приложением), чтобы слив, включая завершение после крайнего срока и сброс телеметрии, успел завершиться до принудительного завершения. Если запас только + 2, запрос, застрявший в долгом запросе к базе в момент крайнего срока, приведёт к тому, что под будет убит через SIGKILL (код выхода 137), а не завершится с кодом 0:
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, чтобы отложить последовательность завершения:
lifecycle:
preStop:
exec:
command: ["sleep", "5"]Это даёт балансировщику нагрузки время убрать под из своего списка целей, прежде чем OxPHP прекратит принимать соединения. Это важно, потому что соединение, направленное на под после сигнала, до конца слива остаётся без ответа, а не отклоняется (см. шаг 1 последовательности завершения).
Docker
Docker отправляет SIGTERM при выполнении docker stop. Стандартный таймаут остановки Docker составляет 10 секунд, после чего Docker отправляет SIGKILL.
Чтобы дать OxPHP достаточно времени на слив, увеличьте таймаут остановки:
docker stop --time 45 my-oxphp-containerИли задайте его в вашем файле Compose:
services:
oxphp:
image: ghcr.io/oxphp/oxphp:0.13.0
stop_grace_period: 45s
environment:
DRAIN_TIMEOUT_SECONDS: "30"Сообщения в логах
Во время корректного завершения работы OxPHP выводит структурированные сообщения в логах, которые можно отслеживать:
Успешный слив:
{"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 — его соединения уже нет, пока выполняется его фоновая работа.
Достигнут крайний срок слива:
{"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 — нужные страницы он прочитает сам.
Сделай так, чтобы моё развёртывание OxPHP завершалось, не теряя запросов. Прочитай страницу `operations/graceful-shutdown` через MCP-сервер документации OxPHP.
Согласуй между собой период отсрочки завершения, хук `preStop` и таймауты завершения в моих манифестах Kubernetes с учётом самого долгого блокирующего вызова (запроса к базе данных или таймаута сокета), допускаемого моим приложением, и скажи мне, что происходит на каждом шаге с запросом, который уже в обработке.См. также
- Проверки работоспособности — взаимодействие проверок готовности (readiness probes) и завершения работы
- Справочник по конфигурации — все переменные окружения, включая
DRAIN_TIMEOUT_SECONDS - Метрики —
oxphp_active_connectionsотслеживает соединения во время слива