Метрики Prometheus
OxPHP отдаёт Prometheus-совместимые метрики в текстовом формате экспозиции (text exposition format) на 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.
Используйте эту гистограмму, чтобы отслеживать общую задержку, выявлять медленные эндпоинты и измерять перцентили хвостовой задержки (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 держится в любом случае.
Сервер тоже замечает заклинивание. Всякий раз, когда два последовательных ежесекундных сканирования видят ожидающую работу (запросы в очереди или растущие отказы) при хотя бы одном простаивающем воркере и пуле, не завершившем ничего, он пишет в лог:
{"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 на том же ежесекундном сканировании и сообщает об эпизоде, а не об отдельных запросах, поскольку строка на каждый отказ сама стала бы отказом в обслуживании. Первое сканирование, заметившее рост любой из них, пишет:
{"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) не выдают себя за нагрузку. Датчика для этого состояния нет, и ни одной пробе о нём не сообщается: сброс запросов никогда не меняет ответ /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-запросы полезны при построении дашбордов:
Частота запросов (запросов в секунду):
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
Добавьте задание сбора (scrape job) в ваш prometheus.yml:
scrape_configs:
- job_name: "oxphp"
scrape_interval: 15s
static_configs:
- targets: ["oxphp:9090"]Для service discovery в 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 - Режим воркеров — постоянные воркеры и метрики, которые они экспортируют