Тайм-аути
OxPHP застосовує два незалежні тайм-аути, які захищають від повільних клієнтів і запитів, що вийшли з-під контролю. Тайм-аут заголовків охороняє фазу з’єднання на рівні сервера. Час виконання PHP обмежує власна ini-директива PHP max_execution_time (а також функція часу виконання set_time_limit()). Одна властивість цього ліміту відрізняється від PHP-FPM так, що це легко проґавити: OxPHP працює з потокобезпечною (ZTS) збіркою PHP, де ліміт вимірює реальний час, а не процесорний. Див. Реальний час, а не процесорний.
Як це працює
Кожен запит проходить такі фази:
- З’єднання прийнято — починається тайм-аут заголовків. OxPHP чекає, поки клієнт надішле повний набір HTTP-заголовків.
- Заголовки отримано — тайм-аут заголовків завершується. Запит передається PHP-воркеру.
- PHP обробляє запит — код застосунку виконується під власним
max_execution_timePHP, який запускається як POSIX-таймер окремого потоку. Коли ліміт досягнуто, запит скасовується і спрацьовує уніфікована фатальна помилкаRequest cancelled (timeout). У режимі воркерів кожен запит має власний ліміт, зокрема й той, який воркер бере, поки вже веде інші, а запит, що на момент вичерпання свого ліміту призупинений в очікуванні, завершується, щойно відновлюється (див. Режим воркерів → Граничні терміни запитів). - Відповідь надіслано — на 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 з’єднаннях обидва тайм-аути застосовуються незалежно до кожного запиту в з’єднанні.
Коли 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 жодного разу не впирався в 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
max_execution_time = 30Або для окремого скрипту під час виконання:
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), щоб продовжити його під час виконання:
// at the top of a slow script
set_time_limit(300);Для SSE- чи стримінгових ендпоінтів, де з’єднання мають залишатися відкритими безстроково, вимкніть таймер для цього скрипту:
set_time_limit(0);З’єднання розриваються до надходження заголовків
Тайм-аут заголовків закороткий для клієнтів на каналах із високою затримкою або за повільними балансувальниками навантаження.
Виправлення: збільште тайм-аут заголовків:
HEADER_TIMEOUT_SECONDS=15Через OPcache перший запит після зміни скрипту завершується тайм-аутом
Перекомпіляція OPcache додає затримку першому запиту після зміни файлу. Найчастіше це трапляється в середовищах розробки з великою кількістю файлів. Збільште max_execution_time або вимкніть його під час розробки.
Приклад для Docker
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 містить:
max_execution_time = 30Найкращі практики
- Ніколи не встановлюйте
max_execution_time = 0глобально в продакшені, якщо тільки у вас немає SSE- чи long-polling-ендпоінтів, яким потрібні безстрокові з’єднання. Надавайте перевагуset_time_limit(0)для окремих скриптів. - Використовуйте коротші ліміти для API-серверів. API мають передбачуваний час відповіді.
max_execution_timeу 30 секунд швидко виявляє завислі запити, не зачіпаючи звичайний трафік. Підбирайте його з огляду на найповільнішу залежність нижче за ланцюжком, а не на процесорну роботу скрипту, бо час очікування теж враховується. - Поєднуйте з обмеженням частоти запитів. Тайм-аути захищають окремі запити; обмеження частоти захищає від великого обсягу запитів. Разом вони покривають і повільні, і швидкі шаблони атак.
Промпти для вашого агента
Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.
Налаштуй тайм-аути OxPHP під мій найдовший запит. Прочитай сторінку `features/timeouts` через MCP-сервер документації OxPHP.
Знайди, що в цьому репозиторії справді виконується найдовше (завантаження, звіти, експорти, а також очікування баз даних і вищестоящих сервісів, адже ліміт рахує реальний час), а потім дай мені тайм-аут заголовків і `max_execution_time`, з якими варто працювати, плюс перевизначення для окремих скриптів для всього, що не вміщується в глобальний ліміт.Дивіться також
- Обмеження частоти запитів — обмеження частоти запитів для кожної IP-адреси
- Server-Sent Events (SSE) — рекомендації щодо вимкнення тайм-аутів для стримінгових ендпоінтів
- Довідник із налаштувань — повний довідник змінних середовища