Тайм-аути

OxPHP застосовує два незалежні тайм-аути, які захищають від повільних клієнтів і запитів, що вийшли з-під контролю. Тайм-аут заголовків охороняє фазу з’єднання на рівні сервера. Час виконання PHP обмежує власна ini-директива PHP max_execution_time (а також функція часу виконання set_time_limit()). Одна властивість цього ліміту відрізняється від PHP-FPM так, що це легко проґавити: OxPHP працює з потокобезпечною (ZTS) збіркою PHP, де ліміт вимірює реальний час, а не процесорний. Див. Реальний час, а не процесорний.

Як це працює

Кожен запит проходить такі фази:

  1. З’єднання прийнято — починається тайм-аут заголовків. OxPHP чекає, поки клієнт надішле повний набір HTTP-заголовків.
  2. Заголовки отримано — тайм-аут заголовків завершується. Запит передається PHP-воркеру.
  3. PHP обробляє запит — код застосунку виконується під власним max_execution_time PHP, який запускається як POSIX-таймер окремого потоку. Коли ліміт досягнуто, запит скасовується і спрацьовує уніфікована фатальна помилка 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, щоб код застосунку міг визначити причину.
  • Виконує всі колбеки register_shutdown_function() точно так, як це робить PHP-FPM.
  • Повертає HTTP 504 Gateway Timeout, а в журнал помилок записується повідомлення Request cancelled (timeout).

Реальний час, а не процесорний

max_execution_time вимірює, скільки часу запит існує, а не скільки процесорного часу він спожив. Враховується кожна секунда очікування: повільний запит до бази даних, HTTP-виклик до іншого сервісу, блокувальний fread() на сокеті, sleep().

Це відрізняється від PHP-FPM, і різниця полягає в збірці PHP, а не в OxPHP. Потокобезпечна (ZTS) збірка, якої OxPHP потребує, бо запускає багато інтерпретаторів PHP як потоки одного процесу, запускає ліміт як POSIX-таймер окремого потоку на монотонному годиннику. Непотокобезпечна збірка, з якою працює PHP-FPM, запускає його через setitimer(ITIMER_PROF) — таймер, що йде лише тоді, коли процес виконується на процесорі. Саме звідси знайоме емпіричне правило «очікування бази даних не зараховується до max_execution_time». Воно справджується для PHP-FPM і не справджується тут.

Наслідок проявляється під час міграції наявного застосунку:

Запит: 25 с очікування звітного запиту, 2 с рендерингу Зараховується до ліміту 30 с
PHP-FPM (непотокобезпечна збірка) ~2 с (ліміт фактично недосяжний)
OxPHP (потокобезпечна збірка) 27 с (один повільний запит може вичерпати ліміт)
Міграція з PHP-FPM

Застосунок, який під PHP-FPM жодного разу не впирався в max_execution_time, після переходу на OxPHP може почати повертати 504 на найповільніших ендпоінтах — без жодної зміни значення директиви. Підбирайте ліміт з огляду на повну затримку запиту, включно з усім, на що запит чекає, і збільшуйте його для ендпоінтів, які блокуються на зовнішніх системах.

Коди статусу скасування

OxPHP скасовує запит з кількох різних причин. Кожна відповідає статусу, що відображає реальну ситуацію, а не загальний 500:

Причина Статус Примітки
Перевищено max_execution_time / set_time_limit() 504 Gateway Timeout Вичерпання часу виконання на боці сервера.
Коректне завершення роботи сервера злило запит 503 Service Unavailable Додає Retry-After: 5, щоб клієнти повторили запит до відновленого або замінного екземпляра.
Клієнт закрив з’єднання посеред запиту 499 «Client Closed Request» у стилі nginx. З’єднання вже немає, тож цей статус з’являється лише в журналах доступу й метриках — у мережу він ніколи не надсилається. Показує перервані клієнтом запити як не-5xx, щоб вони не засмічували сповіщення про серверні помилки.

Це єдині три причини скасування. Запит, який, за спостереженням супервізора, виконується довше за поріг, фіксується через oxphp_worker_stuck_total і oxphp_worker_long_running_total і продовжує виконуватися; сервер ніколи його не скасовує.

Якщо ваш ERROR_PAGES_DIR містить лише 500.html, додайте 504.html, 503.html і (за бажанням) 499.html, щоб стилізовані сторінки були узгодженими для всіх причин скасування.

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

Змінна За замовчуванням Опис
HEADER_TIMEOUT_SECONDS 5 Максимальна кількість секунд на отримання заголовків запиту після прийняття з’єднання. Захищає від атак slowloris. 0 не обробляється особливо — його передано в hyper як нульовий тайм-аут, який спрацьовує негайно. Щоб вимкнути тайм-аут, приберіть змінну, а не встановлюйте її в 0

Час виконання PHP налаштовується через php.ini, а не через змінні середовища 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-сервер 5 с 30 с
Звичайне вебобслуговування 5 с 60 с
Завантаження файлів 10 с 300 с
SSE / long-polling 5 с 0 (вимкнено, задається для кожного скрипту)

Коригуйте ці значення відповідно до характеристик вашого застосунку. Для SSE-ендпоінтів викликайте set_time_limit(0) на початку стримінгового скрипту, а не вимикайте max_execution_time глобально.

Усунення несправностей

Клієнти несподівано отримують 504 з «Request cancelled (timeout)»

Ліміт часу виконання PHP спрацював раніше, ніж скрипт завершився.

Якщо під PHP-FPM ці запити працювали нормально, а після переходу на OxPHP почали падати, найімовірніша причина — семантика таймера, описана в розділі Реальний час, а не процесорний: час, який запит проводить в очікуванні бази даних або вищестоящого сервісу, зараховується до ліміту.

Виправлення: збільште 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.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:ro

Де php.ini містить:

php.ini
max_execution_time = 30

Найкращі практики

  • Ніколи не встановлюйте max_execution_time = 0 глобально в продакшені, якщо тільки у вас немає SSE- чи long-polling-ендпоінтів, яким потрібні безстрокові з’єднання. Надавайте перевагу set_time_limit(0) для окремих скриптів.
  • Використовуйте коротші ліміти для API-серверів. API мають передбачуваний час відповіді. max_execution_time у 30 секунд швидко виявляє завислі запити, не зачіпаючи звичайний трафік. Підбирайте його з огляду на найповільнішу залежність нижче за ланцюжком, а не на процесорну роботу скрипту, бо час очікування теж враховується.
  • Поєднуйте з обмеженням частоти запитів. Тайм-аути захищають окремі запити; обмеження частоти захищає від великого обсягу запитів. Разом вони покривають і повільні, і швидкі шаблони атак.

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

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

prompt
Налаштуй тайм-аути OxPHP під мій найдовший запит. Прочитай сторінку `features/timeouts` через MCP-сервер документації OxPHP. Знайди, що в цьому репозиторії справді виконується найдовше (завантаження, звіти, експорти, а також очікування баз даних і вищестоящих сервісів, адже ліміт рахує реальний час), а потім дай мені тайм-аут заголовків і `max_execution_time`, з якими варто працювати, плюс перевизначення для окремих скриптів для всього, що не вміщується в глобальний ліміт.

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