Prometheus メトリクス

OxPHP は、内部サーバーの GET /metrics で Prometheus 互換のメトリクスをテキスト表示形式(text exposition format)で公開します。これらのメトリクスは、リクエストのスループット、レスポンスタイム、コネクションの状態、ワーカープールの健全性、静的ファイルのキャッシュ、圧縮効率、ワーカーモードのパフォーマンスをカバーします。

メトリクスの有効化

内部サーバーを起動するには 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 に到達するものに限りません)に、そのリクエストへ応答した時点で 1 回カウントされます。したがって client_abort は、リクエストがまだキューにある間にクライアントが切断した場合(よくあるケース)と、ハンドラーの実行中に切断した場合の両方を含みます。レスポンスの送出が始まった後に切断したクライアントはその時点を過ぎているため、カウントされません。常に出力されます
Note

記録されたイベントが 1 つ以上あるメソッドおよびステータスクラスのみが出力されます。カウントがゼロのラベルは省略されます。

リクエスト処理時間のヒストグラム

メトリクス 種類 説明
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 accept ループが MAX_CONNECTIONS の空き許可を待って停止している間は 1。コネクションはカーネルに受け入れられますが、新しいものは何も処理されないため、クライアントから見るとサーバーは応答を止めています。oxphp_accept_stalled == 1 で直接アラートを設定してください。設定された上限と比較する必要はありません
oxphp_accept_stalls_total counter 受け入れ後に MAX_CONNECTIONS の許可を待たなければならなかったコネクション数。停止が 2 回のスクレイプの間に始まって終わった場合でも動くため、ゲージだけでは見逃すものを捉えます。rate(oxphp_accept_stalls_total[5m]) > 0 は、たとえ一瞬でも上限が使い果たされていることを意味します。MAX_CONNECTIONS を引き上げるか、PHP のバックログ(QUEUE_CAPACITY + QUEUE_MAX_WAITING)を縮小してください。accept ループのための余裕を確保する を参照してください
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(リクエストを渡すワーカースレッドが残っていない。プールはビジーなのではなく消滅している)。過負荷なのは最初の 4 つだけで 529 を返します。shutting_down はグレースフルドレインの他の部分と同じく 503 を、pool_unavailable は 500 を返します。過負荷のアラートはその 4 つを明示して設定してください。メトリクス全体は再起動でも動くからです。oxphp_queue_wait_us からは除外されます
oxphp_admission_wait_wasted_total counter プールがまったくリクエストを開始しない間に、待機予算をまるごと待ち切ったリクエスト数。oxphp_admission_refused_total{reason="wait_timeout"} の部分集合で、ワーカーを奪い合う他のリクエストに負けた待機と、プールが何も開始しなかった待機とを区別します。上記と同様、クライアントが既に去っていたリクエストは除外されます。oxphp_admission_wait_budget_us が上限にある間のみカウントされます。0 以外が続く場合、待機は何の役にも立っていません。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 誰も保持していない受け入れ許可の数。リクエストはキューに入る前に許可を 1 つ取り、ワーカーに取り出されたときに返します。つまりこれは、新たに到着したリクエストが待たずに確保できる空き容量です。グレースフルドレイン中はゲートが閉じており、この値にかかわらず何も受け入れません
oxphp_admission_wait_budget_us gauge 今到着したリクエストがワーカーを待てる時間(マイクロ秒。この値が上限となる oxphp_queue_wait_us と同じ単位)。QUEUE_WAIT_TIMEOUT_MS がこの値の上限です。待機によって受け入れた処理が既に去ったクライアントのために完了している間、サーバーはこの値を半分にし、キューが空になってプールが開始する処理のうち去ったクライアント向けのものが 4 分の 1 未満になると、段階的に元へ戻します。上限を下回っているのは、上限より辛抱強くないクライアントに対して、プールが処理しきれない量を提示されていることを意味します。エピソードの間ずっと下限(上限の 64 分の 1。上限自体がそれより短くない限り 10 ms まで引き上げられます)に張り付いているのは正常ですが、oxphp_queue_depth がゼロのまま張り付いているのは正常ではありません。フェイルファストモード(QUEUE_WAIT_TIMEOUT_MS=0)では 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。つまりプールがキューから作業を取り出すのをやめた状態です。この状態が 1 分続くと設定されます(リサイクル後にアプリケーションを再読み込みするワーカーでは立たない程度の長さです)。ワーカーがリクエストを完了するか、プールが詰まって見えない 1 分が経過すると解除されます。oxphp_pool_stalled == 1 で直接アラートを設定してください。readiness プローブも同じ状態で 503 を返します。ワーカーモードでのみ出力され、この状態を監視していない場合は 0 ではなく存在しません。詰まったプールのシグナル を参照してください

キューの深さを受け入れスロットと照らし合わせて読む

2 つのゲージは 1 つの問いの別々の半分に答えます。その問いとは、キューがどれだけ深いかではなく、何かが動いているかどうかです。oxphp_queue_depth は何が待っているかを、oxphp_admission_slots_available はさらに何かを受け入れられるかを示します。どちらも oxphp_busy_workers と照らし合わせて読んでください。

空きスロット キューの深さ 読み方
> 0 0 待っているものはない。正常
> 0 > 0 で、スクレイプ間で減っている 通常のバックログ。プールは遅れているが処理を進めている
> 0 > 0 で減っておらず、oxphp_busy_workers が 0 受け入れにはまだ余裕があるのに、プールがキューから作業を取り出すのをやめている。これは詰まりの第 1 段階です。到着したリクエストは受け入れられ、それぞれ自身の予算が尽きると 529(先にクライアントが諦めた場合は 499)で応答されます。予算が上限にある間、529 のたびに oxphp_admission_wait_wasted_total が増えます。他に手がかりはありません。ビジーなワーカーはおらず、拒否だけを見ると過負荷のように見えます
0 oxphp_queue_capacity に達している キューが満杯になった後の同じ詰まりか、プールが大きく遅れている通常の過負荷。oxphp_busy_workers がスクレイプをまたいで 0 のままかどうかで両者を区別できます。単一のサンプルなら過負荷でもそう読めることがあります
0 0 またはその付近 キューの外で保持されている許可。1 回のスクレイプだけなら受け入れとディスパッチの間にあるリクエストの可能性がありますが、スクレイプをまたいで続く場合は、取得されたまま返却されない許可です。何も受け入れられず、サーバーは再起動されるまですべての PHP リクエストを拒否します

最後の 3 行は、停止したプールが取る形です。いずれの場合も、PHP リクエストが 1 つも完了しないのに、それ以外の様子は健全に見えます。ビジーなワーカーはおらず、liveness は終始、readiness は最初の 1 分間 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 はどちらの場合も高いまま保たれます。

サーバー自身も詰まりに気づきます。1 秒ごとのスキャンが 2 回連続して、待っている作業(キュー内のリクエスト、または増え続ける拒否)があり、少なくとも 1 つのワーカーがアイドルで、プールが何も完了していないことを確認すると、次のログを出力します。

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}}

この状態が続く間はおよそ 1 分に 1 回繰り返され、作業が動き出すと INFO PHP pool is reaching workers again が続きます。フィールドは fields の下にあるため、アラートルールでは .queue_depth ではなく .fields.queue_depth を指定します。誤報を防ぐガードが 3 つあります。1 回ではなく 2 回のスキャンであること。進捗を、クライアントが受け取ったものではなくワーカーが完了したものとして測ること(そのためクライアントの中断が殺到しても、実際どおりビジーなプールとして読まれます)。そして、プールが最初のリクエストを完了するまでは何も報告しないこと(起動時のブートストラップを詰まりと取り違えないため)です。

最後のガードが対象とするのは、プロセスの最初のブートストラップだけです。後から(メモリ上限や Worker::scheduleExit() で)リサイクルされたワーカーはアプリケーションを再びブートし、最初のリクエストを受け取るまではアイドルとして読まれます。そのため、ワーカーが 1 つのプール(あるいは他のワーカーがすべてビジーな大きめのプール)でブートストラップが遅いと、偽の警告が 1 回出て、その後に回復の通知が出ます。同じ瞬間に oxphp_workers_spawned_total が動いていれば、それと判別できます。

この警告はワーカーモードでのみ出力されます。ワーカーが完了したものを数えているのはそのプールだけだからです。ワーカーがキュー自体でブロックするモードでは、この警告が表す状態は起こりえません。3 つのゲージはすべてのモードで出力されます。

無駄になった待機

oxphp_admission_wait_wasted_total は、キューから離れたエントリーではなく開始された作業を数えます。期限で既に応答済みのリクエストをワーカーが片付けても、キューは動きますが誰にもサービスしていないからです。予算が上限にあることを条件にしているのは、サーバーが予算を短縮した後では「プールが何も開始しなかった」がプールよりも時間枠について多くを語るためです。4 ワーカーの健全なプールが 0.25 秒のハンドラーを処理すると 62 ms ごとに 1 件リクエストを開始するので、下限ではほとんどの待機が 2 回の開始の間で終わってしまいます。

この条件によって失われるものは、予算が動くかどうかで決まります。

  • 何も開始しないプールは、コントローラーが判断の基準とする開始リクエスト数に決して達しないため、上限を保ち、このカウンターは有効なままです。
  • 自分自身を呼び出すプールは、外側のリクエストが内側の呼び出しを諦めるたびにリクエストを開始します。自分の呼び出し元がハンドラーより辛抱強くなくなると予算は下がり(数秒以内に 2 回半減したことが測定されています)、このカウンターは下がる前の到着分が終わると静かになります。そこではこのページのどれも代わりを務めません。アプリケーション自身の外向き呼び出しの中からそのパターンを見つけてください。呼び出し元が辛抱強い場合(内部の cron や、タイムアウトに余裕のあるサイドカー)、誰も去らないので上限はそのまま保たれ、このカウンターがそれを示すものになります。

典型的な原因は、アプリケーションが HTTP 経由でこの同じサーバーを呼び返すことですが、このカウンターは自己呼び出しの検出器ではありません。すべてのハンドラーが予算より長く実行されるプールも、待機の間に何も空けないからです。ワーカーモードでは oxphp_worker_request_duration_us(ハンドラーの時間。キュー待機は除く)で両者を区別できます。このカウンターが増えているのにハンドラーが予算を下回っているなら、待機は単に遅い処理の後ろに並んでいるだけではありません。これに oxphp_request_duration_us を使わないでください。これらの拒否を含んでおり、それぞれがほぼ予算まるごと分を加えるからです。ワーカーモード以外では、アプリケーション自身の計測値を使ってください。

QUEUE_WAIT_TIMEOUT_MS=0 は、短い予算をさらに短くしたものではありません。キューのスロットを待つ時間をなくすだけで、キューの中での待機は無制限のままにします。そのため、何も取り出さないプールでは、空きスロットを見つけたリクエストは決して応答されません。0 を使うなら、問題になる頃にはキューが満杯になっている程度に小さな QUEUE_CAPACITY と組み合わせてください。

リクエストを切り捨てている間のログ

スーパーバイザーは同じ 1 秒ごとのスキャンで oxphp_admission_refused_total の過負荷を示す 4 つの理由を監視し、個々のリクエストではなくエピソードを報告します。拒否ごとに 1 行出していたら、それ自体が障害になるからです。いずれかが動いたのを最初に確認したスキャンは次のログを出力します。

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 はそのスキャン 1 回で拒否した数です。ただし、確認できるキューがある最初のスキャンだけは、それ以前に切り捨てたものすべてを報告します(到着が積み上がる中でまだアプリケーションを読み込んでいるプールも、本物のエピソードです)。深さが上限に達して空きスロットがないのは処理が遅れているプールであり、wait_budget_us が wait_budget_ceiling_us を大きく下回っていれば、サーバーが既に待機を短縮していることを示します。両方の数値があるのは、下限まで押し下げられた予算と、そこまで低く設定された予算とは、単独では見分けがつかないからです。キューの数値はスキャン時点のスナップショットで、refused は直前の 1 秒をカバーしているため、空のキューの横に大きな refused があるのは短いバーストであって、矛盾ではありません。

どちらの数も、oxphp_admission_refused_total の過負荷を示す 4 つのラベルによって理由別に内訳が示され、フィールド名は内訳の元になる数の名前から作られます。refused_queue_full、refused_wait_timeout、refused_waiting_full、refused_waiting_bytes の合計が refused になり、後述の繰り返しの警告と締めくくりの行には episode_refused_queue_full とそれに並ぶ 3 つのフィールドが含まれ、その合計が episode_refused になります。どの設定を調整すべきかを決めるのは理由であり(それぞれが何を示すかは上のカウンターの行を参照してください)、そのカウンターは INTERNAL_ADDR でしか提供されません。内部リスナーのないインスタンスでは、これらのフィールドと、トレーシングが有効な場合の oxphp.shed_reason スパン属性が、理由を読み取れる場所として残ります。4 つすべてが 0 であっても出力されます。明示的な 0 こそが、その理由を除外する根拠になるからです。

エピソードが続く間、警告は PHP admission is still shedding requests under overload として繰り返されます。頻度は多くても 1 分に 1 回で、それ自体が拒否しているスキャンでのみ出る(そのため間隔は 2 分近くになることもあります)ほか、スキャンごとの refused と並んで episode_refused(累計)を含みます。繰り返しの警告が理由別の内訳を示すのは、そのスキャンの数ではなくエピソードの累計です。これが重要なのは、締めくくりの行が INFO で書き出され、本番向けの設定が使うログレベル warn より低いためです。そうした環境では、繰り返しの警告が、書き出されたスキャンの時点までにエピソードが何で構成されていたかを示す唯一の行になります。最初の 1 分以内に終わるエピソードには繰り返しの警告がまったくなく、最後の繰り返しより後に拒否されたものは締めくくりの行にしか現れません。拒否のない 1 分でエピソードは終わり、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 はこの行までではなく最後の拒否までの時間なので、締めくくりの静かな 1 分は含まれません。静かな 1 分という条件は、1 分より短い間隔が 1 つのエピソードにまとめられることも意味し、それによって出力は最悪でも 1 分に 2 行に抑えられます。開始にはそのような遅延はありません。拒否はサンプルではなく事実だからです。

この報告はすべてのルーティングモードで出力され、過負荷を示す 4 つの理由だけで動くため、再起動(shutting_down)やプールの消滅(pool_unavailable)が負荷として告げられることはありません。この状態を表すゲージはなく、どのプローブにも伝えられません。切り捨てによって /health/readiness の応答が変わることはありません。529 を切り捨てているインスタンスは、正常に動くプールで素早く応答しているのであり、過負荷のレプリカを順番にローテーションから外していけば、Service にはエンドポイントが 1 つも残らなくなるからです。過負荷には容量の追加で対処してください。詰まったプールも切り捨てを行い、ワーカーモードではその状態が 1 分続くと 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 現在少なくとも 1 つのリクエストを実行しているワーカースレッド数。oxphp_workers_current を超えることはありません。数えるのはスレッドであってリクエストではありません — ワーカーモードでは 1 つのスレッドが多数のリクエストファイバーを多重化しても 1 とカウントされます。受け入れ待ちやキュー内のリクエストはカウントされず、それらは 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。

これはワーカーがリクエストをキューから取り出す瞬間までの、待機に費やされた時間(受け入れ待ち、次いでキュー内)を測るものです。つまり「リクエストにどれだけ時間がかかったか」ではなく「ワーカーが取り出すまでどれだけかかったか」に答えます。その瞬間より後のものは含まれません。ワーカーによるリクエストの準備、スクリプト(ストリームレスポンスではレスポンスヘッドまで)、そしてサーバーの I/O スレッドへのレスポンスの受け渡しは、すべて oxphp_request_duration_us に計上されます。この受け渡しはタダではありません。レスポンスは I/O スレッドが起こされるのを、あるいはその前に積まれた作業が片付くのを待つため、軽いスクリプトではこの待機より後の時間がスクリプト自身の実行時間の数倍になることがあります。

ここに記録されるのは、予算内にワーカーがキューから取り出したリクエストだけです。過負荷で拒否された 529、あるいはサーバーのシャットダウン中やプールの消滅時の 503/500 は oxphp_admission_refused_total にカウントされます。ワーカーが到達する前にクライアントが去った場合の 499 は oxphp_request_cancelled_total{reason="client_abort"} にカウントされ、どの拒否系列にも入りません。

キュー待機時間が長いのは、リクエストがワーカーに届くまでに時間がかかったことを意味しますが、それだけでワーカー不足を意味するわけではありません。どの測定値にも、渡されたリクエストにワーカースレッドが取りかかるまでの時間が含まれており、それは負荷のないサーバーでも存在します。PHP_WORKERS を増やす前に、リクエストがすべてのワーカーがビジーな状態に行き当たっていること、そしてマシンに余っているコアがあることを確認してください。サーバー自身の I/O スレッド、他のプロセス、コンテナの CPU クォータがワーカーと競合します。oxphp_busy_workers と oxphp_workers_current の比較は前者に部分的にしか答えません。スクレイプ時点で読まれるため、スクレイプの間にプールを埋めたバーストはここには現れてもそこには現れず、ワーカーモードではスレッドは何らかのリクエストを抱えている間、そのリクエストのハンドラーが中断されてスレッドが他を引き受けている間でさえ、ビジーとカウントされます。レンジはデフォルトの QUEUE_WAIT_TIMEOUT_MS に合わせて 1 秒まで達するため、実行前に待機予算の大半を費やしたリクエストも +Inf にまとめられるのではなく定量化されます。処理されるリクエストが予算より長く待つことはありません(予算を超えたリクエストは拒否されます)ので、待機を再び +Inf へ押し込む唯一の設定は QUEUE_WAIT_TIMEOUT_MS の引き上げです。

レート制限メトリクス

メトリクス 種類 説明
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 と比較される量です。上限チェックの少し前、終了したリクエストのスーパーグローバルがまだヒープ上にある間に読まれるため、リサイクルを引き起こす値より数 KB 上になります。アラートは上限ちょうどではなく、上限より下で設定してください。一度もリクエストを処理していないワーカーはゼロを示します。ラベル: worker(スロットのインデックス、例: "0", "1"。リサイクルされたワーカーは前任のスロットを再利用し、自身のリクエストを完了するまで前任の最後の値を引き継ぎます)
oxphp_worker_uptime_seconds gauge 各ワーカーが生成されてからの経過秒数。ラベル: worker
oxphp_worker_requests_count gauge 各ワーカーインスタンスが処理したリクエスト数。ラベル: worker
oxphp_worker_request_fibers_active gauge ワーカーが現在抱えているリクエストファイバー数。アイドルなら 0、1 つのリクエストを最初から最後まで処理中なら 1、リクエストが中断されてスレッド上で多重化されている場合はそれ以上です。完了時ではなくワーカーのループが 1 周するたびに更新されるため、リクエストを完了しなくなったワーカーで最後の健全な値のまま固まることはありません。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 に 0 以外の値を設定する必要があり、それぞれに独自の出力条件があります。カウンターは少なくとも 1 つのタスクがディスパッチまたは拒否された後にのみ出力され、_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 最初の試行がコレクターからの応答なしにすぐ失敗したために、1 回リトライされたエクスポート数

エクスポートは、最初の試行が OTEL_EXPORTER_OTLP_TIMEOUT の半分と 1 秒のいずれか短いほうの時間内に、コレクターからの応答なしに失敗した場合、つまり接続がリセットまたはクローズされたか、接続を確立できなかった場合に、1 回リトライされます。よくある原因は、エクスポートの合間にアイドル状態だった接続をロードバランサーや NAT が切断したことです。API キーの拒否やスロットリングなど、コレクターが応答として返したエラーステータスはリトライされません。OTLP 仕様がその大半のリトライを禁じており、またエクスポーターは、スロットリング中のコレクターが求める待機時間を引き継がないからです。バッチを届けたリトライは oxphp_otel_export_retries_total にのみカウントされます。エクスポートのタイムアウトに達した試行はリトライされないため、遅いコレクターに同じバッチが 2 回送られることはありません。

ただし、リトライによって重複が生じることはあります。接続が切れる前にコレクターが最初の試行を受信していた場合、リトライはそれらのスパンを 2 回目として届けます。同じ理由で、失敗としてカウントされたエクスポートが実際にはコレクターに届いていることもあります。

キューが満杯であるためにトレーサーがエクスポート前に破棄したスパン(OTEL_BSP_MAX_QUEUE_SIZE、デフォルトは 2048 スパン)は、ここではカウントされません。

Grafana ダッシュボードのヒント

以下の PromQL クエリはダッシュボードの構築に役立ちます。

リクエストレート(1 秒あたりのリクエスト数):

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 と組み合わせてその深さを確認してください。

キューの飽和(1 秒あたりのドロップレート):

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]))

1 秒あたりの圧縮による節約バイト数:

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 から写した数値ではなく、この比率でアラートを設定してください。

accept ループの停止(0 以外でアラート):

text
rate(oxphp_accept_stalls_total[5m])

ワーカーのリサイクルレート(1 分あたり):

text
rate(oxphp_worker_recycles_total[5m]) * 60

ワーカーの平均メモリ使用量:

text
avg(oxphp_worker_memory_bytes)

Prometheus のスクレイプ設定

prometheus.yml にスクレイプジョブを追加します。

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

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"

エージェント向けプロンプト

これらのプロンプトは、OxPHP ドキュメント MCP サーバー を接続したエージェントを前提としています — 必要なページはエージェントが自分で読み取ります。

prompt
Prometheus が私の OxPHP サービスをスクレイプするようにしてください。OxPHP ドキュメント MCP サーバー経由で `operations/metrics` ページを読んでください。 このリポジトリにすでにあるサービス名を使って、スクレイプ設定と、メトリクスエンドポイントに到達できるようにするために私の Compose や Kubernetes のファイルに必要なものを追加してください。
prompt
OxPHP 向けの出発点となる Grafana ダッシュボードを作ってください。OxPHP ドキュメント MCP サーバー経由で `operations/metrics` ページを読んでください。 私のアプリケーションが健全かどうかを実際に教えてくれるメトリクスを選び、各パネルが動いたときに何を示しているのかを説明してください。

関連情報