内部サーバー

OxPHP は、ヘルスチェック、Prometheus メトリクス、ライブ設定の確認のために、専用ポート上で別個の HTTP サーバーを実行します。このサーバーはメインのアプリケーションリスナーから完全に分離されており、TLS もレート制限もリクエストID もイベント処理もありません。

仕組み

INTERNAL_ADDR にリッスンアドレス(例: 127.0.0.1:9090)を設定すると、OxPHP はそのアドレス上で 2 つ目の HTTP リスナーを起動します。内部サーバーは以下の組み込みエンドポイントを公開します。

  • /health — 集約された JSON ステータス
  • /health/liveness、/healthz(エイリアス) — liveness プローブ
  • /health/readiness、/readyz(エイリアス) — readiness プローブ
  • /health/startup、/startupz(エイリアス) — startup プローブ
  • /metrics — Prometheus 形式の出力
  • /config — 有効なサーバー設定の JSON

プラグインは /__<plugin>/ プレフィックスの下に追加のエンドポイントを登録できます。グレースフルシャットダウンの間、内部サーバーはメインサーバーが接続のドレインを完了するまで利用可能な状態を保ちます。

Note

内部サーバーは INTERNAL_ADDR が明示的に設定されている場合にのみ起動します。設定されていない場合、ヘルス、メトリクス、設定の各エンドポイントは HTTP 経由で利用できません。

設定

Variable Default Description
INTERNAL_ADDR (未設定) 内部サーバーのアドレス。未設定の場合は起動しません。ポートのみの値(:9090)は 127.0.0.1 にバインドします。ホスト外へ公開するには明示的に 0.0.0.0:9090 を使用してください。例: 127.0.0.1:9090
INTERNAL_ALLOW_IPS (未設定) カンマ区切りの CIDR/IP 許可リスト。リスト外のピアは /metrics、/config、/__<plugin>/ パスに対して 403 を受け取ります。ヘルスプローブは常に到達可能なままです。未設定または空の場合はすべて許可されます。ループバックは暗黙的に含まれません。localhost からのアクセスを維持するには 127.0.0.1/32 を列挙してください。不正な形式のリストは起動を中断させます

エンドポイント

GET /health

ダッシュボードやコンテナのヘルスチェック向けに、JSON のヘルスステータスを返します。Kubernetes では、代わりに専用のプローブエンドポイントを使用してください(Kubernetes との統合を参照)。/health はすべてのサブシステムの状態をまとめて報告するため、liveness に接続すると、そのいずれかの障害で Pod が再起動されてしまいます。

200 OK — すべてのシステムが正常:

json
{ "active_connections": 7, "executor_healthy": true, "plugins": { "otel": "ok" }, "pool_stalled": false, "status": "ok", "total_requests": 48203, "uptime_secs": 3612 }

503 Service Unavailable — プラグインが失敗を報告した場合:

json
{ "active_connections": 7, "executor_healthy": true, "plugins": { "otel": "failed" }, "pool_stalled": false, "status": "degraded", "total_requests": 48203, "uptime_secs": 3612 }

plugins オブジェクトは、ロードされたすべてのプラグインをそのヘルスステータス("ok"、"degraded"、"failed")とともに列挙します。いずれかのプラグインが "failed" を報告した場合、稼働中の PHP ワーカースレッドが 1 つも残っていない場合(executor_healthy: false)、またはプールが詰まって動かなくなった場合(pool_stalled: true、ワーカーモードのみ。ヘルスチェックを参照)、このエンドポイントは 503 を返します。"degraded" のプラグインは JSON ボディには表示されますが、HTTP ステータスは 200 のままです。負荷も障害には当たりません。過負荷で 529 を返して負荷を逃がしているサーバーも、"ok" を報告し続けます。

GET /metrics

Prometheus 互換のメトリクスをテキスト形式(text/plain; version=0.0.4; charset=utf-8)で返します。常に 200 を返します。

bash
curl http://localhost:9090/metrics
text
# HELP oxphp_requests_total Total HTTP requests # TYPE oxphp_requests_total counter oxphp_requests_total 48203 # HELP oxphp_active_connections Current open connections # TYPE oxphp_active_connections gauge oxphp_active_connections 7 ...

利用可能なメトリクスの完全な一覧については、メトリクスを参照してください。

GET /config

有効なサーバー設定を JSON として返します。常に 200 を返します。

bash
curl -s http://localhost:9090/config | jq .
json
{ "access_log": "all", "async_in_flight_cap": 0, "async_max_fibers": 256, "async_queue_capacity": 0, "async_workers": 0, "brotli_level": 5, "deny_file": { "allow": 1, "deny": 3, "entry": 1, "fallback": "404", "loaded": true, "rules": 5 }, "document_root": "/var/www/html/public", "drain_timeout_seconds": 25, "entry_file": "/var/www/html/public/index.php", "executor_type": "sapi", "gzip_level": 6, "header_timeout_seconds": 5, "listen_addr": "0.0.0.0:443", "log_level": "info", "max_connections": 10000, "max_query_body": 524288, "php_workers": "8", "plugins": {}, "queue_capacity": 1024, "queue_max_waiting": 1024, "queue_max_waiting_bytes": 67108864, "queue_wait_timeout_ms": 1000, "rate_limit": 100, "rate_window_seconds": 60, "runtime_hooks": ["sleep", "streams", "net"], "static_max_age": 2592000, "static_revalidate": false, "superglobals_enabled": true, "tls_enabled": true, "tls_min_version": "1.2", "tokio_workers": 4, "trace_context": false, "trusted_proxies": false, "worker_max_memory_mib": 0, "worker_mode_enabled": false, "zstd_level": 6 }

runtime_hooks は、プロセスが起動時にインストールしたランタイムフックのカテゴリを列挙します。これは RUNTIME_HOOKS の生の値ではなく解析済みの集合なので、all、1、sleep, streams, net はいずれも ["sleep", "streams", "net"] として表示され、スクレイパーがこの書式を自前で再実装する必要はありません。空のリストはフックが 1 つもインストールされていないことを意味し、変数名のスペルミスでも同じ結果になります。これが報告するのはインストールされたものであって、実際に効いているものではありません。ファイバーがまったく存在しない環境ではフックは置き換えた元の組み込み関数に処理を委ねるため、従来モードのサーバーはセットアップしたカテゴリを列挙しつつ、ネイティブと同じように動作します。ランタイムフックを参照してください。

deny_file は、起動時に DOCUMENT_ROOT から読み込まれた .oxphpdeny ファイルの概要です。アクション別のルール数(deny、> ルールを表す entry、! ルールを表す allow)と、deny ルールが返す応答(PHP_DENY_FALLBACK のステータスコード、またはスクリプトのパスを含まない "script")を示します。ルールそのものは含まれません。外部から到達できる内部サーバーでは、ルールはこのファイルが保護しているパスをそのまま示す地図になってしまうからです。各ルールがどのように読み込まれたかを確認するには、LOG_LEVEL=debug でサーバーを起動し、oxphpdeny rule の行を探してください。ファイルがない場合、このエントリーは {"loaded": false} になります。

TLS 証明書と鍵ファイルのパスが出力されることは決してなく(公開されるのは tls_enabled のブール値のみ)、internal_addr と error_pages_dir は返されるレスポンスから除去されます。これらはデプロイのトポロジーやファイルシステムのパスであり、攻撃者を助けるものであってスクレイパーには不要だからです。

プラグインエンドポイント

プラグインは /__<plugin_name>/ プレフィックスの下にカスタムエンドポイントを登録できます。たとえば、otel という名前のプラグインは /__otel/status を公開できます。これらのエンドポイントは、対応するプラグインがロードされてハンドラーを登録している場合にのみ利用可能です。

組み込みエンドポイントにもプラグインエンドポイントにも一致しないパスは、404 Not Found を返します。

Kubernetes との統合

3 つのプローブすべてを /health に向けるのではなく、それぞれのプローブを専用のエンドポイントに接続してください。/health はすべてのサブシステムを 1 つのステータスにまとめるため、liveness に使うと、プラグインの障害やプールの詰まりに Pod の再起動で応じてしまいます。

readiness プローブ

/health/readiness は、Kubernetes が Pod にトラフィックをルーティングするかどうかを制御します。

yaml
readinessProbe: httpGet: path: /health/readiness port: 9090 initialDelaySeconds: 2 periodSeconds: 5

503 を返すと、Kubernetes は Service のエンドポイント一覧から Pod を除外し、再び 200 を返すとトラフィックが再開されます。このエンドポイントが報告するのは負荷ではなく障害なので、過負荷の Pod はローテーションに残ります。ヘルスチェックを参照してください。

liveness プローブ

/health/liveness は、プロセスが HTTP リクエストを少しでも処理できる限り 200 を返します。liveness が意味すべきなのはまさにこれです。過負荷のプールは再起動では直らず、処理中の作業も巻き添えにしてしまうからです。

yaml
livenessProbe: httpGet: path: /health/liveness port: 9090 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3

startup プローブ

起動が遅いアプリケーション(大規模なフレームワーク、重いオートローダーなど)向け:

yaml
startupProbe: httpGet: path: /health/startup port: 9090 initialDelaySeconds: 1 periodSeconds: 2 failureThreshold: 15

セキュリティ

内部サーバーにはベアラートークン認証がありません。アクセスは、どこにバインドするかと INTERNAL_ALLOW_IPS によって制御されます。保護するには次のようにします。

  • localhost にバインドする — ポートのみの INTERNAL_ADDR(:9090)はすでに 127.0.0.1 にバインドされます。明示的な 127.0.0.1:9090 は、そのポートをコンテナまたはホスト内からのみ到達可能に保ちます
  • リスナーをホスト外から到達可能にする必要がある場合は**INTERNAL_ALLOW_IPS を設定する** — CIDR/IP の許可リストで、リスト外のピアには /metrics、/config、プラグインパスに対して 403 を返しつつ、ヘルスプローブは到達可能なまま保ちます。ループバックは暗黙的に含まれないため、localhost からのアクセスがまだ必要な場合は 127.0.0.1/32 を含めてください。許可リストを設定せずにリスナーがホスト外に公開されている場合、サーバーは起動時に警告します
  • Kubernetes の Service として公開しない — ポートを containerPort として宣言しても、そのための Service は作成しないでください。Kubernetes のプローブはコンテナのポートに直接アクセスします
  • ネットワークポリシーを使用する — ポートを公開しなければならない場合は、ネットワーク層でアクセスを制限してください
Warning

/config エンドポイントは運用上の詳細(ドキュメントルート、レート制限、ワーカー数、タイムアウト値)を明らかにします。TLS のパス、internal_addr、error_pages_dir は除去されますが、残りの情報を Pod の外部からアクセス可能にすべきかどうかを検討してください。

Docker の例

手軽に確認するには内部ポートを公開できます。本番環境では、内部サーバーを localhost にバインドしてください。Kubernetes の場合は、httpGet プローブがループバックのリスナーに到達できないため、0.0.0.0:9090 にバインドして INTERNAL_ALLOW_IPS を設定してください。

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "80:80" - "9090:9090" environment: - DOCUMENT_ROOT=/var/www/html/public - INTERNAL_ADDR=0.0.0.0:9090

トラブルシューティング

ヘルス、メトリクス、設定の各エンドポイントが利用できない

INTERNAL_ADDR が設定されていません。

対処: 環境変数を追加します。

bash
INTERNAL_ADDR=0.0.0.0:9090
/health が 503 を返すがアプリケーションは動作している

ロードされたプラグインが PluginHealth::Failed を報告しているか、まれに executor_healthy が false、または pool_stalled が true になっています。どのプラグインが失敗しているかを特定するには、ヘルスレスポンスの plugins オブジェクトを確認してください。

bash
curl -s http://localhost:9090/health | jq '.plugins'
コンテナの外部から内部サーバーに到達できない

サーバーは 127.0.0.1 にバインドされており、これはコンテナ内部からのみアクセス可能です。

対処: 外部アクセス用に 0.0.0.0 に変更し、INTERNAL_ALLOW_IPS を設定してください。Kubernetes の httpGet プローブはループバックのリスナーに到達できないため、同じ変更が必要です。

メトリクスにワーカーモードや非同期メトリクスが表示されない

ワーカーモードのメトリクスは、ワーカーモードが有効(WORKER_MODE_ENABLED=true)な場合にのみ表示されます。非同期メトリクスは、ASYNC_WORKERS > 0 かつ少なくとも 1 つのタスクがディスパッチまたは拒否された場合にのみ表示されます。

関連項目