Метрики Prometheus

OxPHP отдаёт Prometheus-совместимые метрики в текстовом формате экспозиции (text exposition format) на GET /metrics внутреннего сервера. Они охватывают пропускную способность по запросам, время ответа, состояние соединений, состояние пула воркеров, кеширование статических файлов, эффективность сжатия и производительность режима воркеров.

Включение метрик

Задайте INTERNAL_ADDR, чтобы запустить внутренний сервер:

bash
INTERNAL_ADDR=127.0.0.1:9090

Затем собирайте метрики из Prometheus или любого совместимого коллектора:

bash
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 охватывает как клиента, положившего трубку, пока его запрос ещё стоял в очереди (типичный случай), так и клиента, положившего трубку во время работы обработчика; клиент, отключившийся уже после того, как ответ начал уходить, этот момент миновал и не учитывается. Экспортируется всегда
Note

Экспортируются только методы и классы статусов, по которым зафиксировано хотя бы одно событие. Метки с нулевым значением опускаются.

Гистограмма длительности запросов

Метрика Тип Описание
oxphp_request_duration_us histogram Полная длительность запроса в микросекундах для всех запросов (статические файлы и PHP)

Границы бакетов (микросекунды): 100, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000, 250000, 500000, 1000000, +Inf.

Используйте эту гистограмму, чтобы отслеживать общую задержку, выявлять медленные эндпоинты и измерять перцентили хвостовой задержки (tail latency).

Метрики соединений

Метрика Тип Описание
oxphp_active_connections gauge Открытые в данный момент TCP-соединения на основном порту
oxphp_accept_stalled gauge 1, пока цикл приёма стоит в ожидании свободного разрешения MAX_CONNECTIONS. Ядро продолжает принимать соединения, но ничего нового не обслуживается, так что с точки зрения клиента сервер перестал отвечать. Оповещайте напрямую по oxphp_accept_stalled == 1; сравнивать с настроенным бюджетом не нужно
oxphp_accept_stalls_total counter Соединения, которым после приёма пришлось ждать разрешения MAX_CONNECTIONS. Растёт, даже если остановка началась и закончилась между двумя сборами, чего один датчик не заметил бы: 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 в микросекундах: наибольшее значение, которое датчику выше вообще разрешено принимать, постоянное на всё время жизни процесса. Позволяет оповещению сравнивать бюджет с его потолком, пользуясь только этим эндпоинтом, а не зашитым числом, — та же пара, что oxphp_queue_depth с oxphp_queue_capacity. 0 в режиме немедленного отказа. Отсутствует у исполнителя без очереди
oxphp_abandoned_work_total counter Запросы, которые воркер взял из очереди, когда их клиент уже ушёл. Бюджет ожидания выше подстраивается по этому счётчику. Рост при падающем бюджете — это контроллер, делающий свою работу; рост при бюджете, остающемся на потолке, означает, что клиенты уходят во время работы обработчика, а не во время ожидания, так что сокращать нужно не ожидание. Это число запросов, а не потраченного впустую CPU. Запросы, отклонённые по крайнему сроку, не учитываются (они не выполнялись), как и те, чей ответ ушёл раньше, чем обработчик завершился (поток, заголовки которого уже отправлены, или ранний ответ через oxphp_finish_request()), поэтому окончание каждой SSE-сессии не учитывается. Здесь виден только клиент, закрывший соединение
oxphp_pool_stalled gauge 1, пока запросы ждут воркеров, которые простаивают и ничего не делают, то есть пул перестал забирать работу из очереди. Выставляется, когда состояние продержалось минуту (достаточно долго, чтобы его не вызывал воркер, заново загружающий приложение после перезапуска); сбрасывается, когда воркер завершает запрос, или после минуты, в течение которой пул вообще не выглядел заклинившим. Оповещайте напрямую по oxphp_pool_stalled == 1; проба готовности отвечает 503 при том же состоянии. Экспортируется только в режиме воркеров; там, где состояние не отслеживается, отсутствует, а не равен 0. См. Сигнал заклинившего пула

Глубина очереди в сравнении со слотами допуска

Эти два датчика отвечают на разные половины одного вопроса: движется ли что-нибудь вообще, а не насколько глубока очередь. 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 или около того Разрешения удерживаются вне очереди. В пределах одного сбора это могут быть запросы между допуском и диспетчеризацией; устойчиво на протяжении нескольких сборов это разрешения, взятые и так и не возвращённые: ничего не может быть допущено, и сервер отклоняет каждый 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 держится в любом случае.

Сервер тоже замечает заклинивание. Всякий раз, когда два последовательных ежесекундных сканирования видят ожидающую работу (запросы в очереди или растущие отказы) при хотя бы одном простаивающем воркере и пуле, не завершившем ничего, он пишет в лог:

json
{"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, растущий в тот же момент.

Предупреждение выдаётся только в режиме воркеров, поскольку только этот пул ведёт счёт того, что воркеры завершили; описываемое им состояние не может возникнуть там, где воркер блокируется на самой очереди. Три датчика экспортируются во всех режимах.

Напрасные ожидания

oxphp_admission_wait_wasted_total считает начатую работу, а не записи, покидающие очередь, потому что воркер, вычищающий запросы, на которые уже ответили их крайние сроки, двигает очередь, никого не обслуживая. Счётчик работает только при бюджете на потолке, потому что, когда сервер уже сократил бюджет, «пул не начал ничего» говорит больше об окне, чем о пуле: здоровый пул из четырёх воркеров с обработчиками по четверти секунды начинает по запросу каждые 62 мс, так что на нижней границе большинство ожиданий заканчивается между двумя запусками.

Чего стоит такое ограничение, зависит от того, двигается ли бюджет:

  • Пул, который не начинает ничего, никогда не набирает того числа начатых запросов, по которому решает контроллер, поэтому держится на потолке, и этот счётчик остаётся взведённым.
  • Пул, вызывающий сам себя, начинает запрос всякий раз, когда внешний запрос бросает свой внутренний вызов. Как только его собственные вызывающие становятся менее терпеливыми, чем его обработчики, бюджет снижается (измерено: два уполовинивания за секунды), и этот счётчик замолкает после запросов, пришедших до снижения. Ничто на этой странице не берёт это на себя: ищите закономерность в исходящих вызовах самого приложения. Там, где вызывающие терпеливы (внутренний cron, сайдкар с щедрым таймаутом), никто не уходит, потолок стоит, и именно этот счётчик указывает на проблему.

Классическая причина — приложение, обращающееся по HTTP обратно к этому же серверу, но счётчик не является детектором самовызовов: пул, все обработчики которого работают дольше бюджета, тоже ничего не освобождает за время ожидания. В режиме воркеров их различает oxphp_worker_request_duration_us (время обработчика без ожидания в очереди): обработчики короче бюджета при растущем счётчике означают, что ожидания не просто стоят в очереди за медленной работой. Не используйте для этого oxphp_request_duration_us: он включает эти отказы, каждый из которых вносит почти целый бюджет. Вне режима воркеров пользуйтесь собственными замерами приложения.

QUEUE_WAIT_TIMEOUT_MS=0 — не самый короткий вариант сокращённого бюджета: он убирает ожидание слота очереди и оставляет ожидание внутри очереди неограниченным, так что в пуле, который ничего не забирает, запрос, нашедший свободный слот, никогда не получит ответа. Сочетайте 0 с достаточно маленьким QUEUE_CAPACITY, чтобы очередь была заполнена к тому моменту, когда это станет важно.

Что пишет лог, пока запросы сбрасываются

Супервизор следит за четырьмя причинами перегрузки из oxphp_admission_refused_total на том же ежесекундном сканировании и сообщает об эпизоде, а не об отдельных запросах, поскольку строка на каждый отказ сама стала бы отказом в обслуживании. Первое сканирование, заметившее рост любой из них, пишет:

json
{"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:

json
{"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) не выдают себя за нагрузку. Датчика для этого состояния нет, и ни одной пробе о нём не сообщается: сброс запросов никогда не меняет ответ /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 Всего мягких сбросов, выполненных между запросами

Датчики по отдельным воркерам

Метрика Тип Описание
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 ненулевое значение, и у каждой есть собственное условие экспорта: счётчики появляются только после того, как хотя бы одна задача была направлена в пул или отклонена, датчики _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-запросы полезны при построении дашбордов:

Частота запросов (запросов в секунду):

text
rate(oxphp_requests_total[5m])

Среднее время ответа (миллисекунды):

text
rate(oxphp_request_duration_us_sum[5m]) / rate(oxphp_requests_total[5m]) / 1000

p99 длительности запроса (миллисекунды):

text
histogram_quantile(0.99, rate(oxphp_request_duration_us_bucket[5m])) / 1000

Доля ошибок (ответы 5xx в процентах):

text
rate(oxphp_responses_by_status_total{status="5xx"}[5m]) / rate(oxphp_requests_total[5m]) * 100

Загрузка пула воркеров:

text
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, чтобы видеть, насколько он глубок.

Насыщение очереди (частота отбрасывания в секунду):

text
rate(oxphp_dropped_requests_total[5m])

p99 ожидания в очереди (микросекунды):

text
histogram_quantile(0.99, rate(oxphp_queue_wait_us_bucket[5m]))

Доля попаданий в кеш статических файлов:

text
rate(oxphp_static_cache_hits_total[5m]) / (rate(oxphp_static_cache_hits_total[5m]) + rate(oxphp_static_cache_misses_total[5m]))

Байты, сэкономленные сжатием, в секунду:

text
rate(oxphp_compression_bytes_saved_total[5m])

p99 задержки в режиме воркеров (микросекунды):

text
histogram_quantile(0.99, rate(oxphp_worker_request_duration_us_bucket[5m]))

Бюджет ожидания, опущенный ниже потолка (доля):

text
oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us

1 — норма; всё, что ниже, означает, что сервер сокращает ожидания под перегрузкой. Оповещайте по этому отношению, а не по числу, скопированному из QUEUE_WAIT_TIMEOUT_MS.

Остановки цикла приёма (оповещать при ненулевом значении):

text
rate(oxphp_accept_stalls_total[5m])

Частота перезапусков воркеров (в минуту):

text
rate(oxphp_worker_recycles_total[5m]) * 60

Среднее использование памяти воркером:

text
avg(oxphp_worker_memory_bytes)

Конфигурация сбора для Prometheus

Добавьте задание сбора (scrape job) в ваш prometheus.yml:

prometheus.yml
scrape_configs: - job_name: "oxphp" scrape_interval: 15s static_configs: - targets: ["oxphp:9090"]

Для service discovery в Kubernetes:

prometheus.yml
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 — нужные страницы он прочитает сам.

prompt
Заставь Prometheus собирать метрики с моего сервиса на OxPHP. Прочитай страницу `operations/metrics` через MCP-сервер документации OxPHP. Добавь конфигурацию скрейпа и всё, что нужно в моих файлах Compose или Kubernetes, чтобы эндпоинт метрик был доступен, — используя имена сервисов, которые уже есть в этом репозитории.
prompt
Собери мне стартовый дашборд Grafana для OxPHP. Прочитай страницу `operations/metrics` через MCP-сервер документации OxPHP. Выбери метрики, которые действительно показывают, исправно ли работает моё приложение, и объясни, о чём говорит каждая панель, когда её значение уходит в сторону.

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

Нашли ошибку? Сообщите →