Метрики Prometheus
OxPHP надає Prometheus-сумісні метрики в текстовому форматі експозиції за адресою GET /metrics на внутрішньому сервері. Вони охоплюють пропускну здатність запитів, час відповіді, стан з’єднань, стан пулу воркерів, кешування статичних файлів, ефективність стиснення та продуктивність режиму воркерів.
Увімкнення метрик
Задайте INTERNAL_ADDR, щоб запустити внутрішній сервер:
INTERNAL_ADDR=127.0.0.1:9090Після цього збирайте метрики з Prometheus або будь-якого сумісного колектора:
curl http://localhost:9090/metricsМетрики сервера
| Метрика | Тип | Опис |
|---|---|---|
oxphp_uptime_seconds |
gauge | Кількість секунд від запуску процесу сервера |
oxphp_requests_total |
counter | Загальна кількість HTTP-запитів, отриманих на основному порту |
Метрики запитів
| Метрика | Тип | Опис |
|---|---|---|
oxphp_requests_by_method_total |
counter | Запити за HTTP-методом. Мітка: method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, QUERY, OTHER) |
oxphp_responses_by_status_total |
counter | Відповіді за класом статусу. Мітка: status (1xx, 2xx, 3xx, 4xx, 5xx) |
oxphp_request_bytes_total |
counter | Загальна кількість отриманих байтів тіла запитів |
oxphp_response_bytes_total |
counter | Загальна кількість надісланих байтів тіла відповідей |
oxphp_request_cancelled_total |
counter | Скасовані запити за причиною. Мітка: reason (client_abort, timeout, shutdown). Враховується один раз для кожного запиту, який сервер диспетчеризує (а не лише для тих, що доходять до PHP), у момент, коли на цей запит надається відповідь. Тож client_abort охоплює і клієнта, що від’єднався, поки його запит ще стояв у черзі (типовий випадок), і клієнта, що від’єднався під час роботи обробника; клієнт, який від’єднується вже після того, як відповідь почала надсилатися, цей момент минув і не враховується. Надається завжди |
Надаються лише методи й класи статусів, для яких зафіксовано хоча б одну подію. Мітки з нульовим лічильником пропускаються.
Гістограма тривалості запитів
| Метрика | Тип | Опис |
|---|---|---|
oxphp_request_duration_us |
histogram | Наскрізна тривалість запиту в мікросекундах для всіх запитів (статичних файлів і PHP) |
Межі бакетів (мікросекунди): 100, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000, +Inf.
Використовуйте цю гістограму, щоб відстежувати загальну затримку, знаходити повільні ендпоінти й вимірювати перцентилі хвостової затримки.
Метрики з’єднань
| Метрика | Тип | Опис |
|---|---|---|
oxphp_active_connections |
gauge | TCP-з’єднання, відкриті зараз на основному порту |
oxphp_accept_stalled |
gauge | 1, поки цикл прийняття з’єднань стоїть в очікуванні вільного дозволу MAX_CONNECTIONS. Ядро приймає з’єднання, але нічого нового не обслуговується, тож для клієнта сервер перестав відповідати. Налаштовуйте сповіщення безпосередньо на oxphp_accept_stalled == 1; порівнювати з налаштованим бюджетом не потрібно |
oxphp_accept_stalls_total |
counter | З’єднання, яким після прийняття довелося чекати на дозвіл MAX_CONNECTIONS. Змінюється навіть тоді, коли зупинка починається й закінчується між двома зборами метрик, чого сам gauge не помітив би: rate(oxphp_accept_stalls_total[5m]) > 0 означає, що бюджет вичерпується, хай навіть ненадовго. Збільште MAX_CONNECTIONS або зменште чергу PHP (QUEUE_CAPACITY + QUEUE_MAX_WAITING); див. Як зберегти запас для циклу прийняття |
oxphp_pending_requests |
gauge | PHP-запити, прийняті, але ще без відповіді: ті, що чекають на місце в черзі, стоять у черзі або виконуються. Лише запити, спрямовані до PHP: статичний файл, 404 чи заборонений шлях отримують відповідь в обхід черги й тут не з’являються |
oxphp_dropped_requests_total |
counter | Запити, на яких PHP-воркер зазнав збою вже після прийняття запиту |
oxphp_admission_refused_total |
counter | Запити, що отримали відповідь, так і не дійшовши до воркера. Мітка: reason зі значеннями wait_timeout (запит вичерпав увесь свій бюджет очікування, тобто QUEUE_WAIT_TIMEOUT_MS або менше, якщо oxphp_admission_wait_budget_us показує, що сервер його скоротив. Дайте пулу більше запасу, але спершу перегляньте oxphp_admission_wait_wasted_total: якщо за час цих очікувань пул не почав жодного запиту, потрібне протилежне рішення. Запит, клієнт якого вже пішов на момент вичерпання бюджету, тут не враховується: він отримує відповідь 499 і враховується в oxphp_request_cancelled_total{reason="client_abort"}, тож ця серія залишається твердженням саме про пул), waiting_full (в очікуванні вже QUEUE_MAX_WAITING запитів; збільште це значення або MAX_CONNECTIONS), waiting_bytes (тіла вже відкладених запитів заповнюють QUEUE_MAX_WAITING_BYTES, тож тілу цього запиту ніде розміститися; збільште це значення або зменште обсяг, який клієнт може завантажити), queue_full (QUEUE_WAIT_TIMEOUT_MS=0, очікування вимкнено), shutting_down (дедлайн дренажу минув, поки запит ще чекав на допуск), pool_unavailable (не залишилося жодного потоку воркера, якому можна передати запит: пулу немає, а не він зайнятий). Перевантаженням є лише перші чотири, і вони відповідають 529; shutting_down відповідає 503, як і решта плавного дренажу, а pool_unavailable відповідає 500. Сповіщення про перевантаження налаштовуйте саме на ці чотири: метрика загалом змінюється й під час перезапуску. Не входить до oxphp_queue_wait_us |
oxphp_admission_wait_wasted_total |
counter | Запити, що вичерпали весь свій бюджет очікування, поки пул не почав жодного запиту. Підмножина oxphp_admission_refused_total{reason="wait_timeout"}, яка відокремлює очікування, програне іншим запитам у конкуренції за воркер, від очікування, за яке пул нічого не почав. Запити, клієнт яких уже пішов, виключено, як описано вище. Враховується, лише поки oxphp_admission_wait_budget_us на своїй стелі. Стабільно ненульове значення означає, що очікування нічого не дає: скоротіть QUEUE_WAIT_TIMEOUT_MS. Див. Марні очікування |
oxphp_queue_depth |
gauge | Запити, що зараз перебувають у черзі воркерів: допущені, але ще не взяті воркером. Запис залишає чергу лише тоді, коли його бере воркер, тож тут враховуються і запити, на які вже відповіли за дедлайном (529 або 499, якщо клієнт пішов), і ті, що досі чекають; глибина понад oxphp_pending_requests і є цією різницею. Присутня лише для SAPI-виконавця, єдиного з чергою |
oxphp_queue_capacity |
gauge | Загальна кількість місць у черзі: QUEUE_CAPACITY, межа, відносно якої читається oxphp_queue_depth |
oxphp_admission_slots_available |
gauge | Дозволи на допуск, які ніхто не тримає. Запит бере один перед входом у чергу й повертає, коли його бере воркер, тож це вільна місткість, яку новий запит може отримати без очікування. Під час плавного дренажу шлюз закритий і не допускає нічого, хоч би що показувала ця метрика |
oxphp_admission_wait_budget_us |
gauge | Скільки запит, що надходить зараз, може чекати на воркер, у мікросекундах (одиниця oxphp_queue_wait_us, яку ця метрика обмежує). QUEUE_WAIT_TIMEOUT_MS — стеля цього значення: сервер зменшує його вдвічі, поки робота, яку допускає очікування, завершується для клієнтів, що вже пішли, і поступово повертає вгору, щойно черга спорожніє й менше чверті того, що починає пул, припадає на клієнтів, які пішли. Значення нижче стелі означає, що пулу пропонують більше, ніж він здатен обслужити, від клієнтів, менш терплячих за стелю. Перебування на мінімумі (одна шістдесят четверта стелі, піднята до 10 мс, якщо сама стеля не коротша) упродовж епізоду — нормальне явище; перебування там з oxphp_queue_depth, що дорівнює нулю, — ні. 0 у режимі швидкої відмови (QUEUE_WAIT_TIMEOUT_MS=0). Відсутня для виконавця без черги. Див. Адаптивний бюджет очікування |
oxphp_admission_wait_budget_ceiling_us |
gauge | QUEUE_WAIT_TIMEOUT_MS у мікросекундах: найбільше значення, якого може досягти gauge вище, стале впродовж усього життя процесу. Дає змогу сповіщенню порівнювати бюджет зі стелею лише за даними цього ендпоінта, а не з жорстко закодованим числом, — та сама пара, що й oxphp_queue_depth з oxphp_queue_capacity. 0 у режимі швидкої відмови. Відсутня для виконавця без черги |
oxphp_abandoned_work_total |
counter | Запити, які воркер узяв із черги, коли їхній клієнт уже пішов. Бюджет очікування вище коригується саме за цим лічильником. Зростання, поки бюджет падає, означає, що регулятор виконує свою роботу; зростання, коли бюджет досі на стелі, означає, що клієнти йдуть під час роботи обробника, а не під час очікування, тож скорочувати треба не очікування. Це кількість запитів, а не змарнованого процесорного часу. Запити, відхилені за дедлайном, не враховуються (вони ніколи не виконувалися), як і ті, відповідь на які пішла до завершення обробника (потік, заголовки якого вже надіслано, або рання відповідь через oxphp_finish_request()), тож завершення кожної SSE-сесії не враховується. Тут видно лише клієнта, який закриває своє з’єднання |
oxphp_pool_stalled |
gauge | 1, поки запити чекають на воркери, що простоюють і нічого не виконують, тобто пул перестав забирати роботу з черги. Встановлюється, коли такий стан триває хвилину (достатньо довго, щоб воркер, який після перезапуску повторно завантажує застосунок, його не спричинив); скидається, коли воркер завершує запит, або після хвилини, упродовж якої пул жодного разу не виглядав заклиненим. Налаштовуйте сповіщення безпосередньо на oxphp_pool_stalled == 1; проба готовності відповідає 503 за того самого стану. Надається лише в режимі воркерів; там, де цей стан не відстежується, метрика відсутня, а не дорівнює 0. Див. Сигнал заклиненого пулу |
Як читати глибину черги разом із місцями допуску
Ці два gauge відповідають на дві половини одного питання: чи щось рухається, а не наскільки глибока черга. oxphp_queue_depth показує, що чекає; oxphp_admission_slots_available — чи можна допустити ще щось. Читайте обидва разом з oxphp_busy_workers.
| Доступні місця | Глибина черги | Тлумачення |
|---|---|---|
> 0 |
0 |
Нічого не чекає; норма |
> 0 |
> 0 і зменшується між зборами метрик |
Звичайне відставання: пул не встигає, але розбирає чергу |
> 0 |
> 0 і не зменшується, а oxphp_busy_workers дорівнює 0 |
Пул перестав забирати роботу з черги, хоча допуск ще має місце. Це заклинювання в першій фазі. Нові запити допускаються, і кожен отримує 529, коли спливає його власний бюджет (або 499, якщо його клієнт здався раніше); oxphp_admission_wait_wasted_total зростає з кожною 529, поки бюджет на стелі. Більше ніщо цього не видає: жоден воркер не зайнятий, а самі відмови виглядають як перевантаження |
0 |
дорівнює oxphp_queue_capacity |
Те саме заклинювання після заповнення черги або звичайне перевантаження, за якого пул сильно відстає. Їх розрізняє oxphp_busy_workers, що стабільно дорівнює 0 упродовж кількох зборів; одна вибірка може так виглядати й під час перевантаження |
0 |
0 або близько до 0 |
Дозволи утримуються поза чергою. В одному зборі це можуть бути запити між допуском і диспетчеризацією; якщо це триває кілька зборів, дозволи взято й ніколи не повернено: нічого не може бути допущено, і сервер відхилятиме кожен PHP-запит, доки його не перезапустять |
Останні три рядки — це форми, яких набуває заклинений пул. У кожній з них решта картини виглядає здоровою, хоча жоден PHP-запит не завершується: жодного зайнятого воркера, 200 від проби життєздатності весь час і від проби готовності впродовж першої хвилини, статичні файли обслуговуються як завжди. Лічильники відмов змінюються, але вони змінюються й за звичайного перевантаження. oxphp_pending_requests зростає разом із застряглою чергою, а потім знову падає, бо кожен новий запит отримує відповідь за своїм бюджетом, тож сама по собі ця метрика тут нічого не означає; у зіставленні з oxphp_queue_depth — означає: pending близько нуля, коли глибина тримається на рівні місткості, — це заклинювання, що утримує все, що йому колись дали.
Заклинювання — саме той стан, через який клієнти йдуть. За балансувальником навантаження, тайм-аут читання якого коротший за QUEUE_WAIT_TIMEOUT_MS, нові запити перетворюються на 499, що не змінюють жодної серії відмов, тож oxphp_admission_wait_wasted_total може мовчати впродовж усього інциденту. Такі запити потрапляють до oxphp_request_cancelled_total{reason="client_abort"}, а oxphp_queue_depth тримається в будь-якому разі.
Сервер теж помічає заклинювання. Щоразу, коли два поспіль односекундні скани бачать роботу, що чекає (запити в черзі або зростання відмов), при цьому принаймні один воркер простоює, а пул нічого не завершив, він пише в журнал:
{"timestamp":"2026-08-28T10:56:59.015691Z","level":"WARN","fields":{"message":"PHP requests are waiting while the pool has idle workers and got nothing done since the last scan","queue_depth":22,"queue_capacity":512,"admission_slots_available":490,"workers_idle":4}}Повідомлення повторюється приблизно раз на хвилину, поки триває стан, а коли робота зрушить, з’являється INFO PHP pool is reaching workers again. Поля розміщено під fields, тож правилу сповіщення потрібне .fields.queue_depth, а не .queue_depth. Хибним повідомленням запобігають три запобіжники: два скани замість одного; прогрес вимірюється тим, що воркери завершили, а не тим, що отримали клієнти, тож шквал обривів з боку клієнтів читається як зайнятий пул, яким він і є; і нічого не повідомляється, доки пул не завершить свій перший запит, тож початкове завантаження не приймається за заклинювання.
Останній запобіжник стосується лише першого завантаження процесу. Воркер, перезапущений пізніше (через стелю пам’яті чи Worker::scheduleExit()), знову завантажує свій застосунок і вважається таким, що простоює, доки не візьме перший запит, тож повільне завантаження в пулі з одного воркера (або у більшому пулі, всі інші воркери якого зайняті) дає одне хибне попередження з подальшим повідомленням про відновлення. Одночасна зміна oxphp_workers_spawned_total вказує саме на цей випадок.
Попередження надається лише в режимі воркерів, бо лише цей пул веде облік того, що завершили воркери; описаний стан не може виникнути там, де воркер блокується безпосередньо на черзі. Три gauge надаються в кожному режимі.
Марні очікування
oxphp_admission_wait_wasted_total рахує розпочату роботу, а не записи, що залишають чергу, бо воркер, який прибирає запити, на які вже відповіли їхні дедлайни, рухає чергу, нікого не обслуговуючи. Лічильник працює лише тоді, коли бюджет на стелі, бо після того, як сервер скоротив бюджет, «пул нічого не почав» говорить більше про вікно, ніж про пул: здоровий пул із чотирьох воркерів з обробниками на чверть секунди починає один запит кожні 62 мс, тож на мінімумі більшість очікувань закінчуються між двома стартами.
Чого коштує ця умова, залежить від того, чи змінюється бюджет:
- Пул, який нічого не починає, ніколи не досягає кількості розпочатих запитів, за якою ухвалює рішення регулятор, тож тримає свою стелю, і цей лічильник залишається активним.
- Пул, що викликає сам себе, починає запит щоразу, коли зовнішній запит відмовляється від свого внутрішнього виклику. Щойно його власні клієнти стають менш терплячими за його обробники, бюджет знижується (виміряно два зменшення вдвічі за лічені секунди), і цей лічильник замовкає після запитів, що надійшли до зниження. Жодна метрика на цій сторінці там не бере естафету: шукайте цей шаблон у вихідних викликах самого застосунку. Там, де клієнти терплячі (внутрішній cron, sidecar з великим тайм-аутом), ніхто не йде, стеля тримається, і саме цей лічильник вказує на проблему.
Класична причина — застосунок, що звертається по HTTP до цього ж сервера, але лічильник не є детектором самовикликів: пул, усі обробники якого працюють довше за бюджет, так само нічого не звільняє під час очікування. У режимі воркерів їх розрізняє oxphp_worker_request_duration_us (час обробника без очікування в черзі): обробники коротші за бюджет, поки цей лічильник зростає, означають, що очікування не просто стоять у черзі за повільною роботою. Не використовуйте для цього oxphp_request_duration_us: вона включає ці відмови, кожна з яких додає майже цілий бюджет. Поза режимом воркерів користуйтеся власними вимірюваннями застосунку.
QUEUE_WAIT_TIMEOUT_MS=0 — не найкоротший варіант скороченого бюджету: воно прибирає очікування на місце в черзі й залишає необмеженим очікування всередині черги, тож у пулі, який нічого не бере, запит, що знайшов вільне місце, так і не отримає відповіді. Поєднуйте 0 з QUEUE_CAPACITY, достатньо малим, щоб черга була заповнена до того, як це стане важливим.
Що пише журнал, поки запити відкидаються
Супервізор стежить за чотирма причинами перевантаження в oxphp_admission_refused_total під час того самого односекундного сканування й повідомляє про епізод, а не про окремі запити, бо рядок на кожну відмову сам по собі став би збоєм. Перше сканування, яке бачить зміну будь-якої з них, пише:
{"timestamp":"2026-09-16T11:02:41.337104Z","level":"WARN","fields":{"message":"PHP admission has started shedding requests under overload","refused":4812,"refused_queue_full":0,"refused_wait_timeout":4812,"refused_waiting_full":0,"refused_waiting_bytes":0,"queue_depth":896,"queue_capacity":896,"admission_slots_available":0,"wait_budget_us":15625,"wait_budget_ceiling_us":1000000}}refused — це те, що відхилило саме це сканування, за винятком першого сканування, якому є на яку чергу дивитися: воно повідомляє про все відкинуте до нього (пул, що ще завантажує застосунок, поки накопичуються запити, — це справжній епізод). Глибина на рівні місткості без жодного вільного місця означає пул, що відстає від роботи, а wait_budget_us, значно менший за wait_budget_ceiling_us, означає, що сервер уже скоротив очікування; обидва числа наведено, бо бюджет, доведений до мінімуму, і бюджет, налаштований так низько, окремо виглядають однаково. Показники черги — це знімок на момент сканування, тоді як refused охоплює щойно минулу секунду, тож великий refused поруч із порожньою чергою — це короткий сплеск, а не суперечність.
Обидва лічильники розбито за причинами — на чотири мітки перевантаження oxphp_admission_refused_total, а назви полів утворено від лічильника, який вони розбивають: refused_queue_full, refused_wait_timeout, refused_waiting_full і refused_waiting_bytes у сумі дають refused, а повторне попередження й завершальний рядок нижче містять episode_refused_queue_full і три подібні поля, які в сумі дають episode_refused. Саме причина визначає, яке налаштування змінювати (рядок цього лічильника вище пояснює, на що вказує кожна), а сам лічильник надається лише на INTERNAL_ADDR. На інстансі без внутрішнього слухача причину ще можна прочитати в цих полях і, якщо ввімкнено трасування, в атрибуті спану oxphp.shed_reason. Усі чотири поля записуються навіть із нульовим значенням: саме явний 0 виключає причину.
Поки триває епізод, попередження повторюється як PHP admission is still shedding requests under overload, не частіше ніж раз на хвилину й лише під час сканування, яке саме щось відхиляє (тож проміжок може сягати майже двох хвилин), і містить episode_refused (накопичений підсумок) поруч із refused за сканування. Повторне попередження розбиває за причинами підсумок епізоду, а не сканування. Це важливо, бо завершальний рядок пишеться на рівні INFO, нижчому за warn, з яким пишуть журнал продакшн-конфігурації, тож там лише повторне попередження показує, з чого досі складався епізод — аж до сканування, під час якого його записано. Епізод, що завершується протягом першої хвилини, взагалі не має повторного попередження, а те, що відхилено після останнього повтору, потрапляє лише в завершальний рядок. Хвилина без жодної відмови завершує епізод на рівні INFO:
{"timestamp":"2026-09-16T11:05:52.812440Z","level":"INFO","fields":{"message":"PHP admission has stopped shedding requests","episode_refused":18374,"episode_refused_queue_full":0,"episode_refused_wait_timeout":17950,"episode_refused_waiting_full":424,"episode_refused_waiting_bytes":0,"duration_ms":131000,"queue_depth":0,"admission_slots_available":896}}duration_ms рахується до останньої відмови, а не до цього рядка, тож завершальна тиха хвилина не враховується. Тиха хвилина також означає, що проміжки, коротші за хвилину, об’єднуються в один епізод, що обмежує вивід щонайбільше двома рядками на хвилину; вхід в епізод такої затримки не має, бо відмова — це факт, а не вибірка.
Звіт надається в кожному режимі маршрутизації й реагує лише на чотири причини перевантаження, тож перезапуск (shutting_down) чи зниклий пул (pool_unavailable) не видають себе за навантаження. Gauge для цього стану немає, і жодна проба про нього не дізнається: відкидання запитів ніколи не змінює відповіді /health/readiness, бо інстанс, що відкидає запити з 529, швидко відповідає з робочим пулом, а виведення з ротації кожної перевантаженої репліки по черзі залишило б Service без жодного ендпоінта. На перевантаження відповідайте додаванням потужностей. Заклинений пул теж відкидає запити, і в режимі воркерів цей стан таки дає 503, щойно протриває хвилину. Розрізняє їх черга: пул, що просто відстає, її розбирає, а заклинений — ні.
Метрики пулу воркерів
| Метрика | Тип | Опис |
|---|---|---|
oxphp_workers_current |
gauge | Поточна кількість потоків PHP-воркерів |
oxphp_workers_min |
gauge | Мінімальна кількість воркерів (у статичному режимі дорівнює поточній) |
oxphp_workers_max |
gauge | Максимальна кількість воркерів (у статичному режимі дорівнює поточній) |
oxphp_workers_idle |
gauge | Потоки воркерів без запиту в обробці, обчислюється як workers_current - busy_workers |
oxphp_busy_workers |
gauge | Потоки воркерів, що зараз виконують принаймні один запит; ніколи не перевищує oxphp_workers_current. Рахує потоки, а не запити: у режимі воркерів один потік мультиплексує багато файберів запитів і все одно враховується один раз. Запити, що чекають на допуск або стоять у черзі, не враховуються; вони відображаються в oxphp_pending_requests |
oxphp_workers_spawned_total |
counter | Загальна кількість воркерів, створених від запуску (включно з початковими) |
oxphp_workers_retired_total |
counter | Загальна кількість воркерів, виведених через тайм-аут простою (лише в динамічному режимі) |
Метрики супервізора воркерів
Спостережуваність кожного воркера, яку надає супервізор воркерів. Кожна серія має мітку worker_id (індекс слота). Вони з’являються, щойно супервізор починає відстежувати стан кожного воркера.
| Метрика | Тип | Опис |
|---|---|---|
oxphp_worker_request_age_seconds |
gauge | Вік запиту в обробці на кожному воркері, у секундах. Мітка: worker_id |
oxphp_worker_long_running_total |
counter | Сканування супервізора, які виявили запит, старший за поріг застрягання (60 секунд, фіксовано). Мітка: worker_id |
oxphp_worker_stuck_total |
counter | Лічильник класифікації застрягань для кожного воркера. Мітки: worker_id, kind (io, c_call, cpu). Лише для звітності: запит, класифікований як застряглий, не скасовується, а його воркер не перезапускається. Сервер ніяк на це не реагує, тож налаштуйте сповіщення на цю серію |
Гістограма очікування в черзі
| Метрика | Тип | Опис |
|---|---|---|
oxphp_queue_wait_us |
histogram | Час, який запит чекає в черзі, доки його не візьме воркер, у мікросекундах |
Межі бакетів (мікросекунди): 50, 100, 250, 500, 1000, 2500, 5000, 10000, 50000, 100000, 250000, 500000, 1000000, +Inf.
Метрика вимірює час очікування — спершу на допуск, потім у черзі — аж до моменту, коли воркер забирає запит із черги, тож вона відповідає на питання «скільки минуло, перш ніж воркер це взяв», а не «скільки тривав запит». Усе, що відбувається після цього моменту, до неї не входить: підготовка запиту воркером, скрипт (до заголовка відповіді, якщо відповідь потокова) і передача відповіді назад до потоків введення-виведення сервера — усе це враховується в oxphp_request_duration_us. Ця передача не безкоштовна: відповідь чекає, доки потік введення-виведення прокинеться або впорається з роботою, що стоїть у черзі перед нею, тож для легкого скрипту час після цього очікування може в кілька разів перевищувати час роботи самого скрипту.
Тут фіксується лише запит, який воркер узяв із черги в межах свого бюджету. 529, відхилена через перевантаження, або 503/500, коли сервер завершує роботу чи пулу немає, враховуються в oxphp_admission_refused_total; 499 для клієнта, що пішов раніше, ніж до нього дійшов воркер, враховується в oxphp_request_cancelled_total{reason="client_abort"} і в жодній серії відмов.
Високе очікування в черзі означає, що запити довго не доходили до воркера, і це саме по собі не свідчить про нестачу воркерів: кожне значення включає час, за який потік воркера дістається до переданого йому запиту, а він є й на ненавантаженому сервері. Перш ніж збільшувати PHP_WORKERS, переконайтеся, що запити застають усіх воркерів зайнятими і що на машині є вільні ядра: з воркерами конкурують власні потоки введення-виведення сервера, інші процеси й квота CPU контейнера. oxphp_busy_workers у зіставленні з oxphp_workers_current відповідає на перше лише частково: значення зчитується в момент збору, тож сплеск, що заповнює пул між зборами, видно тут, а там — ні, і в режимі воркерів потік вважається зайнятим, поки несе будь-який запит, навіть коли обробник цього запиту призупинено, а потік бере нові. Діапазон сягає однієї секунди, що відповідає типовому QUEUE_WAIT_TIMEOUT_MS, тож запит, який витратив більшу частину свого бюджету очікування до виконання, отримує кількісну оцінку, а не потрапляє скопом у +Inf. Жоден обслужений запит не чекає довше за бюджет (після нього запит відхиляється), тож збільшення QUEUE_WAIT_TIMEOUT_MS — єдине налаштування, яке повертає очікування в +Inf.
Метрики обмеження частоти запитів
| Метрика | Тип | Опис |
|---|---|---|
oxphp_rate_limited_total |
counter | Запити, відхилені обмежувачем частоти (повернуто 429) |
oxphp_php_deny_total |
counter | Запити, заблоковані PHP_DENY_PATHS (виконання .php заборонено). Див. Список заборони виконання PHP |
oxphp_path_deny_total |
counter | Запити, заборонені правилом .oxphpdeny. Запит, який забороняє також PHP_DENY_PATHS, враховується лише тут. Див. Правила заборони шляхів |
Метрики кешу статичних файлів
| Метрика | Тип | Опис |
|---|---|---|
oxphp_static_cache_hits_total |
counter | Запити статичних файлів, обслужені з кешу в пам’яті |
oxphp_static_cache_misses_total |
counter | Запити статичних файлів, для яких знадобилося читання з диска |
Метрики стиснення
| Метрика | Тип | Опис |
|---|---|---|
oxphp_compressed_responses_total |
counter | Відповіді, надіслані з кодуванням вмісту (Brotli, zstd або gzip) |
oxphp_compression_bytes_saved_total |
counter | Загальна кількість байтів, заощаджених стисненням (початковий розмір мінус стиснений) |
Метрики режиму воркерів
Ці метрики надаються лише тоді, коли активний режим воркерів (WORKER_MODE_ENABLED=true).
Глобальні лічильники
| Метрика | Тип | Опис |
|---|---|---|
oxphp_worker_mode_enabled |
gauge | Завжди 1, коли активний режим воркерів |
oxphp_worker_requests_handled_total |
counter | Загальна кількість запитів, оброблених постійними воркерами |
oxphp_worker_recycles_total |
counter | Загальна кількість перезапусків воркерів: цикл воркера завершився з однієї з наведених нижче причин. Коли динамічний пул виводить воркер, що простоює, це масштабування вниз, а не перезапуск: воно враховується в oxphp_workers_retired_total, а не тут і не в серії нижче |
oxphp_worker_recycles_by_reason_total |
counter | Перезапуски за причиною. Мітка: reason (scheduled, max_memory, error) |
oxphp_worker_soft_resets_total |
counter | Загальна кількість м’яких скидань між запитами |
Gauge для кожного воркера
| Метрика | Тип | Опис |
|---|---|---|
oxphp_worker_memory_bytes |
gauge | Купа PHP, яку утримує кожен воркер наприкінці свого останнього запиту. Це те, що повертає memory_get_usage() усередині обробника, і величина, з якою порівнюється WORKER_MAX_MEMORY_MIB. Зчитується трохи раніше за перевірку цієї стелі, поки суперглобальні змінні завершеного запиту ще в купі, тож вона на кілька КБ вища за значення, що спричиняє перезапуск: ставте поріг сповіщення нижче стелі, а не на ній. Воркер, що ще не обслужив жодного запиту, показує нуль. Мітка: worker (індекс слота, наприклад "0", "1"; перезапущений воркер повторно використовує слот попередника та його останнє значення, доки не завершить власний запит) |
oxphp_worker_uptime_seconds |
gauge | Секунди від створення кожного воркера. Мітка: worker |
oxphp_worker_requests_count |
gauge | Запити, оброблені кожним екземпляром воркера. Мітка: worker |
oxphp_worker_request_fibers_active |
gauge | Файбери запитів, які воркер несе зараз: 0 — простій, 1 — обслуговує один запит від початку до кінця, більше — там, де запити призупиняються й мультиплексуються в потоці. Оновлюється на кожному оберті циклу воркера, а не після завершення, тож не застигає на останньому здоровому значенні у воркера, що перестав завершувати запити. Воркер допускає щонайбільше 256 файберів запитів, і той, що тримається на 256, перестав забирати роботу з черги. Читайте разом з oxphp_busy_workers: 256 файберів на воркері, який пул вважає таким, що простоює, — це файбери, що пережили свої (вже оброблені) запити й так і не були звільнені, тоді як 256 на зайнятому воркері — це стільки ж запитів в обробці. Мітка: worker |
Гістограма тривалості запитів воркера
| Метрика | Тип | Опис |
|---|---|---|
oxphp_worker_request_duration_us |
histogram | Час виконання PHP-обробника на запит у мікросекундах (лише в режимі воркерів) |
Межі бакетів (мікросекунди): 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, +Inf.
Ця гістограма вимірює час усередині колбека PHP-обробника без урахування очікування в черзі. Використовуйте її, щоб знаходити повільні обробники й відстежувати хвостову затримку в режимі воркерів.
Метрики асинхронного пулу
Ці метрики потребують ненульового значення ASYNC_WORKERS, і кожна має власну умову появи: лічильники з’являються лише після того, як принаймні одне завдання було диспетчеризовано або відхилено, gauge _in_flight / _in_flight_limit з’являються, щойно пул підключить свій лічильник завдань в обробці, а oxphp_async_output_discarded_bytes_total з’являється лише після того, як якийсь вивід було відкинуто.
| Метрика | Тип | Опис |
|---|---|---|
oxphp_async_tasks_dispatched_total |
counter | Загальна кількість асинхронних завдань, переданих у фоновий пул |
oxphp_async_tasks_completed_total |
counter | Асинхронні завдання, що успішно завершилися |
oxphp_async_tasks_failed_total |
counter | Асинхронні завдання, що викинули виняток |
oxphp_async_tasks_cancelled_total |
counter | Асинхронні завдання, які було скасовано |
oxphp_async_tasks_rejected_total |
counter | Асинхронні завдання, відхилені під час диспетчеризації, бо черга пулу заповнена або досягнуто ліміту завдань в обробці (ASYNC_MAX_FIBERS × ASYNC_WORKERS) |
oxphp_async_tasks_stranded_total |
counter | Воркери, що продовжують працювати після тайм-ауту await_race / await_any. Кожне таке завдання може подовжити RSHUTDOWN до 5 секунд. |
oxphp_async_tasks_in_flight |
gauge | Асинхронні завдання, що зараз стоять у черзі або виконуються (надається, щойно пул підключить свій лічильник завдань в обробці) |
oxphp_async_tasks_in_flight_limit |
gauge | Максимальна кількість одночасних асинхронних завдань (ASYNC_MAX_FIBERS × ASYNC_WORKERS) |
oxphp_async_output_discarded_bytes_total |
counter | Байти виводу асинхронних завдань, відкинуті, коли воркер простоює (echo в асинхронному завданні не має клієнта, який би його отримав) |
Метрики експорту OpenTelemetry
Ці метрики потребують OTEL_ENABLED=true. Вони рахують пакети спанів, які OxPHP надсилає колектору OTLP: спани запитів, а також спани APM, коли плагін APM увімкнено.
| Метрика | Тип | Опис |
|---|---|---|
oxphp_otel_export_failures_total |
counter | Експорти спанів, що завершилися невдачею, — після повторної спроби, якщо її було зроблено. Їхні спани більше не надсилаються |
oxphp_otel_export_failed_spans_total |
counter | Спани, які несли ці невдалі експорти |
oxphp_otel_export_retries_total |
counter | Експорти, повторені один раз, бо перша спроба швидко завершилася невдачею, не отримавши відповіді від колектора |
Експорт повторюється один раз, коли його перша спроба завершується невдачею без відповіді від колектора протягом половини OTEL_EXPORTER_OTLP_TIMEOUT або однієї секунди, залежно від того, що коротше: з’єднання було скинуто чи закрито або його не вдалося встановити. Звична причина — балансувальник навантаження чи NAT, що розірвав з’єднання, поки воно простоювало між експортами. Статус помилки, яким відповідає сам колектор, як-от відхилений API-ключ чи обмеження частоти, не повторюється: специфікація OTLP забороняє повторювати більшість із них, а експортер не передає затримку, про яку просить колектор, що обмежує частоту. Повторна спроба, що доставила пакет, враховується лише в oxphp_otel_export_retries_total. Спроба, що впирається в тайм-аут експорту, не повторюється, тож повільному колектору ніколи не надсилається той самий пакет двічі.
Утім, повторна спроба може дублювати дані. Коли колектор таки отримав першу спробу до того, як з’єднання обірвалося, повторна спроба доставляє ці спани вдруге, і з тієї ж причини експорт, урахований як невдалий, міг усе ж дійти до колектора.
Спани, які трасувальник відкидає ще до експорту, бо його черга заповнена (OTEL_BSP_MAX_QUEUE_SIZE, типово 2048 спанів), тут не враховуються.
Поради щодо дашбордів Grafana
Для побудови дашбордів корисні такі запити PromQL:
Частота запитів (запитів на секунду):
rate(oxphp_requests_total[5m])Середній час відповіді (мілісекунди):
rate(oxphp_request_duration_us_sum[5m])
/ rate(oxphp_requests_total[5m]) / 1000p99 тривалості запиту (мілісекунди):
histogram_quantile(0.99, rate(oxphp_request_duration_us_bucket[5m])) / 1000Частка помилок (відповіді 5xx у відсотках):
rate(oxphp_responses_by_status_total{status="5xx"}[5m])
/ rate(oxphp_requests_total[5m]) * 100Завантаженість пулу воркерів:
oxphp_busy_workers / oxphp_workers_currentЦе справжня частка між 0 і 1. Стабільне значення 1 означає, що кожен воркер зайнятий, а нові запити стають у чергу. Поєднуйте її з rate(oxphp_admission_refused_total{reason=~"queue_full|wait_timeout|waiting_full|waiting_bytes"}[5m]), щоб побачити, чи перетворюється це відставання на відмови, і з oxphp_pending_requests, щоб побачити його глибину.
Насиченість черги (частота втрат на секунду):
rate(oxphp_dropped_requests_total[5m])p99 очікування в черзі (мікросекунди):
histogram_quantile(0.99, rate(oxphp_queue_wait_us_bucket[5m]))Частка влучань у кеш статичних файлів:
rate(oxphp_static_cache_hits_total[5m])
/ (rate(oxphp_static_cache_hits_total[5m]) + rate(oxphp_static_cache_misses_total[5m]))Байти, заощаджені стисненням, на секунду:
rate(oxphp_compression_bytes_saved_total[5m])p99 затримки в режимі воркерів (мікросекунди):
histogram_quantile(0.99, rate(oxphp_worker_request_duration_us_bucket[5m]))Бюджет очікування, знижений нижче стелі (частка):
oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us1 — норма; будь-яке менше значення означає, що сервер скорочує очікування через перевантаження. Налаштовуйте сповіщення на це співвідношення, а не на число, скопійоване з QUEUE_WAIT_TIMEOUT_MS.
Зупинки циклу прийняття (сповіщення за ненульового значення):
rate(oxphp_accept_stalls_total[5m])Частота перезапусків воркерів (на хвилину):
rate(oxphp_worker_recycles_total[5m]) * 60Середнє використання пам’яті воркерами:
avg(oxphp_worker_memory_bytes)Конфігурація збору Prometheus
Додайте завдання збору до свого prometheus.yml:
scrape_configs:
- job_name: "oxphp"
scrape_interval: 15s
static_configs:
- targets: ["oxphp:9090"]Для виявлення сервісів у Kubernetes:
scrape_configs:
- job_name: "oxphp"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: oxphp
action: keep
- source_labels: [__meta_kubernetes_pod_ip]
target_label: __address__
replacement: "$1:9090"Промпти для вашого агента
Ці промпти розраховано на агента з підключеним MCP-сервером документації OxPHP: потрібні сторінки він прочитає сам.
Налаштуй збір метрик Prometheus з мого сервісу OxPHP. Прочитай сторінку `operations/metrics` через MCP-сервер документації OxPHP.
Додай конфігурацію збору й усе, що потрібно в моїх файлах Compose або Kubernetes, щоб ендпоінт метрик був досяжний, використовуючи назви сервісів, які вже є в цьому репозиторії.Збудуй мені стартовий дашборд Grafana для OxPHP. Прочитай сторінку `operations/metrics` через MCP-сервер документації OxPHP.
Вибери метрики, які справді показують, чи здоровий мій застосунок, і поясни, про що говоритиме кожна панель, коли її значення зміниться.Див. також
- Перевірки стану — ендпоінти
/healthі/configна внутрішньому сервері - Довідник конфігурації — усі змінні середовища, включно з
INTERNAL_ADDR - Плавне завершення роботи — як дренаж з’єднань впливає на
oxphp_active_connections - Режим воркерів — постійні воркери та метрики, які вони надають