Таймауты

OxPHP применяет два независимых таймаута, которые защищают от медленных клиентов и зависших запросов. Таймаут заголовков ограничивает фазу установления соединения на уровне сервера. Время выполнения PHP ограничивается собственной ini-директивой PHP max_execution_time (и функцией времени выполнения set_time_limit()), ровно так же, как и в любом другом SAPI.

Как это работает

Каждый запрос проходит через следующие фазы:

  1. Соединение принято — запускается таймаут заголовков. OxPHP ждёт, пока клиент пришлёт полный набор HTTP-заголовков.
  2. Заголовки получены — таймаут заголовков завершается. Запрос передаётся PHP-воркеру.
  3. PHP обрабатывает запрос — код приложения выполняется под собственным max_execution_time PHP (на основе SIGALRM). Когда лимит достигнут, запрос отменяется и срабатывает унифицированная фатальная ошибка Request cancelled (timeout).
  4. Ответ отправлен — на keep-alive-соединениях цикл повторяется с шага 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

На keep-alive-соединениях оба таймаута применяются независимо к каждому запросу в рамках соединения.

Note

Когда включён TLS, таймаут заголовков запускается после завершения TLS-рукопожатия, а не в момент принятия TCP-соединения.

Таймаут заголовков защищает от атак типа slowloris, когда клиент отправляет заголовки по одному байту за раз, чтобы удерживать соединения открытыми неограниченно долго.

Отсчёт времени выполнения PHP полностью делегируется PHP. Когда max_execution_time превышен, унифицированный путь отмены в OxPHP:

  • Устанавливает connection_status() & PHP_CONNECTION_TIMEOUT, чтобы код в userland мог определить причину.
  • Выполняет все колбэки register_shutdown_function(), ровно так же, как это делает PHP-FPM.
  • Возвращает HTTP 504 Gateway Timeout, записывая сообщение Request cancelled (timeout) в лог ошибок.

Статус-коды отмены

OxPHP отменяет запрос по нескольким разным причинам. Каждая из них отображается в статус-код на уровне протокола, который отражает фактическое состояние, а не обобщённый 500:

Причина Статус Примечания
Превышен max_execution_time / set_time_limit() 504 Gateway Timeout Исчерпание времени выполнения на стороне сервера.
Корректное завершение работы сервера слило запрос 503 Service Unavailable Добавляет Retry-After: 5, чтобы клиенты повторили запрос к восстановленному или заменяющему экземпляру.
Клиент закрыл соединение в середине запроса 499 «Client Closed Request» в стиле nginx. Соединение уже разорвано, поэтому этот статус появляется только в логах доступа и метриках — на провод он никогда не записывается. Отражает инициированные клиентом обрывы как не-5xx, чтобы они не засоряли алерты о серверных ошибках.
Воркер признан застрявшим супервизором 500 Internal Server Error Обобщённая серверная ошибка — причина (взаимоблокировка, заблокированный системный вызов, …) неизвестна.
Отмена, инициированная в userland 500 Internal Server Error Userland может установить собственный статус через http_response_code() перед запуском отмены; этот явно заданный статус сохраняется.

Если ваш ERROR_PAGES_DIR содержит только 500.html, добавьте 504.html, 503.html и (опционально) 499.html, чтобы стилизованные страницы оставались единообразными для всех причин отмены.

Конфигурация

Переменная По умолчанию Описание
HEADER_TIMEOUT_SECONDS 5 Максимальное число секунд на получение заголовков запроса после принятия соединения. Защищает от атак slowloris. 0 не обрабатывается особым образом — он передаётся в hyper как нулевой таймаут, который срабатывает немедленно. Чтобы отключить таймаут, снимите переменную, а не устанавливайте её в 0

Время выполнения PHP настраивается через php.ini, а не через env-переменные OxPHP:

php.ini
; php.ini max_execution_time = 30

Или для отдельного скрипта во время выполнения:

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

Рекомендуемые значения

Сценарий Таймаут заголовков max_execution_time
API-сервер 5s 30s
Общая веб-раздача 5s 60s
Загрузка файлов 10s 300s
SSE / long-polling 5s 0 (отключено, задавать для каждого скрипта)

Подбирайте эти значения исходя из характеристик вашего приложения. Для SSE-эндпоинтов вызывайте set_time_limit(0) в начале потокового скрипта, а не отключайте max_execution_time глобально.

Устранение неполадок

Клиенты неожиданно получают 504 с «Request cancelled (timeout)»

Лимит времени выполнения PHP сработал раньше, чем скрипт завершился.

Решение: Повысьте max_execution_time для затронутого скрипта или вызовите set_time_limit($seconds), чтобы продлить его во время выполнения:

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

Для SSE- или потоковых эндпоинтов, где соединения должны оставаться открытыми неограниченно долго, отключите таймер для этого скрипта:

php
set_time_limit(0);
Соединения обрываются до прихода заголовков

Таймаут заголовков слишком короток для клиентов на каналах с высокой задержкой или за медленными балансировщиками нагрузки.

Решение: Увеличьте таймаут заголовков:

bash
HEADER_TIMEOUT_SECONDS=15
OPcache приводит к таймауту первого запроса после изменения скрипта

Перекомпиляция OPcache добавляет задержку на первом запросе после изменения файла. Это чаще встречается в средах разработки с большим числом файлов. Повысьте max_execution_time или отключите его на время разработки.

Пример для Docker

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

С php.ini, содержащим:

php.ini
max_execution_time = 30

Рекомендуемые практики

  • Никогда не устанавливайте max_execution_time = 0 глобально в продакшене, если только у вас нет SSE- или long-polling-эндпоинтов, которым требуются неограниченные по времени соединения. Предпочитайте set_time_limit(0) для каждого скрипта отдельно.
  • Используйте более короткие лимиты для API-серверов. У API предсказуемое время ответа. 30-секундный max_execution_time быстро отлавливает застрявшие запросы, не влияя на обычный трафик.
  • Сочетайте с ограничением частоты запросов. Таймауты защищают отдельные запросы; ограничение частоты запросов защищает от большого объёма запросов. Вместе они покрывают как медленные, так и быстрые шаблоны атак.

Смотрите также