アーキテクチャ概要

OxPHP は、従来の nginx + PHP-FPM スタックを置き換える単一バイナリの HTTP サーバーです。HTTP のパース、TLS 終端、ルーティング、PHP の実行、圧縮、そして可観測性を、実行時に外部依存を一切持たない単一のプロセス内で処理します。

OxPHP の仕組み

OxPHP は、2 つのランタイムレイヤーを単一のプロセス内で組み合わせます。

  1. 非同期 HTTP レイヤー。 イベント駆動のネットワークレイヤーが TCP 接続を受け付け、TLS ハンドシェイクを実行し、HTTP リクエストをパースし、レスポンスを送信します。ノンブロッキング I/O を使って数千の同時接続を処理するため、1 つの遅いクライアントが他のクライアントをブロックすることは決してありません。
  2. PHP ワーカープール。 専用の PHP ワーカーのプールが、あなたの PHP スクリプトを実行します。標準モードでは、各ワーカーは一度に 1 つのリクエストを処理します。ワーカーモードファイバー多重化を有効にすると、1 つのワーカーが複数の同時リクエストを処理できます。スクリプトが oxphp_sleep() または oxphp_async_await() を呼び出すと、ファイバーはスレッドを譲り、ワーカーは次のリクエストに切り替わります。
  3. 非同期プール(オプション)。oxphp_async() 経由で投入されたタスク用の独立した OS スレッドです。ASYNC_WORKERS > 0 を設定すると有効になります。ワーカープールから隔離されているため、バックグラウンドタスクが HTTP リクエストの処理をブロックすることはありません。

2 つのレイヤーは有界キューを介して通信します。PHP の実行を必要とする HTTP リクエストが到着すると、非同期レイヤーはそれをキューに入れます。空いている PHP ワーカーがそれを取り出してスクリプトを実行し、クライアントへ配信するためにレスポンスを非同期レイヤーへ返します。

この分離により、ネットワーク I/O(接続の受け付け、ヘッダーの読み取り、レスポンスの圧縮、静的ファイルの配信)が PHP の実行とリソースを奪い合うことは決してありません。各レイヤーは独立してスケールします。

ワーカープール

PHP ワーカープールは、いくつの PHP スクリプトを並行して実行できるかを決定します。OxPHP は 2 つのプールモードをサポートします。

静的プール

固定数のワーカーが起動時に立ち上がり、サーバーの稼働中はずっと動き続けます。これがデフォルトのモードです。

bash
PHP_WORKERS=8 # exactly 8 workers PHP_WORKERS=0 # auto-detect (default): half of available CPU cores, minimum 1

動的プール

ワーカーは需要に応じてスケールアップおよびスケールダウンします。最小数と最大数をコロンで区切って指定します。

bash
PHP_WORKERS=2:16 # start with 2, scale up to 16 under load

現在のすべてのワーカーがビジー状態になると、OxPHP は最大数まで新しいワーカーを生成します。ワーカーが PHP_WORKERS_IDLE_SECONDS(デフォルト: 30 秒)より長くアイドル状態になると、そのワーカーは最小数に向けて縮退されます。

キューとバックプレッシャー

非同期 HTTP レイヤーとワーカープールの間には有界キューが存在します。その容量はデフォルトで初期ワーカー数に 128 を掛けた値で、QUEUE_CAPACITY で上書きできます。静的プールの場合、初期数は設定されたワーカー数です。動的プール(MIN:MAX)の場合、初期数は最小値です。

キューが満杯(すべてのワーカーがビジーで、かつキューが容量に達した状態)になると、OxPHP は直ちに Retry-After ヘッダー付きの 529 Site is Overloaded レスポンスをクライアントへ返します。ステータスコード 529(非標準で、Cloudflare などが使用)は、過負荷をアプリケーションエラー(500)やメンテナンス(503)と明確に区別するため、アラートやロードバランサーの設定が容易になります。

リクエストの流れ

静的ファイルを配信するか PHP を実行するかにかかわらず、すべてのリクエストは同じパイプラインを通過します。

graph TD
  Client(["Client"]) --> TLS["TLS termination<br/>(if configured)"]
  TLS --> Parse["HTTP parsing + Request ID"]
  Parse --> Proxy["Trusted proxy resolution<br/>(if TRUSTED_PROXIES set)"]
  Proxy --> Rate["Rate limiting check"]
  Rate --> Route{"Route resolution"}
  Route -->|Static file| Cache["File cache / disk read"]
  Cache --> Compress["Compression + Response headers"]
  Route -->|PHP request| Queue["Bounded queue<br/>(529 if full)"]
  Queue --> Worker["PHP worker executes script"]
  Worker --> Normal["Normal response"]
  Worker --> SSE["SSE streaming (chunked)"]
  Worker --> Early["Early response (finish_request)<br/>+ background work"]
  Compress --> Deliver(["Response to client"])
  Normal --> Deliver
  SSE --> Deliver
  Early --> Deliver
  1. TLS 終端。 TLS_CERTTLS_KEY が設定されている場合、OxPHP は TLS を直接処理します。別途リバースプロキシは必要ありません。
  2. HTTP のパースとリクエストID。 リクエストがパースされ、一意のリクエストIDが生成されます(または受信した X-Request-ID ヘッダーが保持されます)。
  3. 信頼済みプロキシの解決。 TRUSTED_PROXIES が設定されていて、接続元の IP が信頼済みである場合、OxPHP は Forwarded(RFC 7239)または X-Forwarded-* ヘッダーから、実際のクライアント IP、プロトコル、ホストを抽出します。解決された IP は、レート制限やアクセスログを含む以降のすべてのステップで使用されます。信頼済みプロキシを参照してください。
  4. レート制限。 RATE_LIMIT が設定されている場合、クライアントの IP が IP ごとのリクエストカウンターと照合されます。制限を超えたリクエストは、直ちに 429 Too Many Requests レスポンスを受け取ります。
  5. ルートの解決。 URL は、設定されたルーティングモード(traditional、framework、または SPA)と照合されます。結果は、静的ファイル、PHP スクリプト、または 404 のいずれかです。ワーカーモードが有効な場合、解決されたスクリプトを PHP がどのように実行するかは変わりますが、ルートの解決そのものは変わりません。
  6. 静的ファイル。 インメモリキャッシュ(頻繁にアクセスされるファイル用)から直接配信されるか、ディスクからストリーミングされます。OxPHP は ETagLast-ModifiedCache-Control ヘッダーを自動的に付加します。
  7. PHP の実行。 リクエストは有界キューに入れられ、空いているワーカーによって取り出されます。キューが満杯の場合、クライアントは直ちに 529 を受け取ります。
  8. 圧縮。 クライアントが Accept-Encoding: br を送信した場合、テキストベースのレスポンスは送信前に Brotli で圧縮されます(COMPRESSION_LEVEL で設定可能)。
  9. SSE ストリーミング。 スクリプトが Content-Type: text/event-stream を設定するか、oxphp_stream_flush() を呼び出すと、OxPHP はストリーミングモードに切り替わります。各 flush() 呼び出しは、レスポンス全体をバッファリングすることなく、直ちにチャンクをクライアントへ送信します。ワーカーモードでは、SSE はファイバー多重化と協調して動作します。
  10. 早期レスポンス。 oxphp_finish_request() を呼び出すと、HTTP レスポンスが直ちにクライアントへ送信されます。スクリプトは接続を開いたままにすることなく、バックグラウンドで実行を続けます(ログの書き込み、キャッシュの更新、通知の送信など)。
  11. レスポンスの配信。 完成したレスポンスは接続を通じて返送され、アクセスログが有効な場合はログエントリが書き込まれます。

ワーカーモードと標準モードの比較

OxPHP は 2 つの PHP 実行モデルをサポートします。

標準モード(デフォルト)

リクエストごとに新しい PHP 環境を作成します。オートローダー、設定、データベース接続はリクエストごとに初期化され、その後に破棄されます。このモデルは、すべての PHP アプリケーションと、追加設定なしでそのまま互換性があります。

ワーカーモード

リクエストをまたいで PHP プロセスを生かし続けます。アプリケーションは一度だけブートストラップし(オートローダーと設定を読み込み、データベース接続を確立)、その後リクエストループに入ります。リクエストの合間に、OxPHP はブートストラップされた状態を保持したまま、スーパーグローバル、出力バッファ、レスポンスヘッダーを自動的にリセットします。

ワーカーモードはリクエストごとの起動オーバーヘッドを排除します。これにより、ブートストラップのコストが高いフレームワークベースのアプリケーション(Laravel、Symfony など)では、レスポンスタイムを大幅に短縮できます。

ワーカーモードを有効にするには、WORKER_MODE_ENABLED=true を設定し、ENTRY_FILEoxphp_worker() を呼び出す PHP スクリプトに向けます。

php
<?php require __DIR__ . '/../vendor/autoload.php'; $app = new MyApp\Application(); oxphp_worker(function () use ($app) { $app->handle(); });

詳細なガイドについては、ワーカーモードを参照してください。

内部サーバー

INTERNAL_ADDR 変数が設定されている場合、OxPHP は指定されたポートで別個の HTTP サーバーを起動します。このサーバーは 3 つのエンドポイントを提供します。

エンドポイント 説明
GET /health JSON 形式のヘルスステータス(稼働時間、リクエストカウンター、接続数、ワーカーの状態)。通常運用時は 200、劣化時は 503 を返します。
GET /metrics Prometheus 形式のメトリクス — リクエストカウンター、レスポンスタイム、キュー待機時間、ワーカーの統計情報、圧縮による削減量。
GET /config JSON 形式のアクティブな設定のスナップショット。TLS のファイルパスは秘匿されます。

内部サーバーは、PHP ワーカープールや有界キューを経由しません。非同期 HTTP レイヤーから直接応答するため、PHP プールが完全に負荷状態にあってもアクセス可能なままです。このため、/health は Kubernetes の liveness/readiness プローブに適しています。

詳細については、内部サーバーを参照してください。

安全性

OxPHP は、本番環境でアプリケーションを確実に稼働させ続けるために、いくつかの保証を提供します。

  • リクエストの分離。 PHP スクリプトがクラッシュしたり致命的なエラーを引き起こしたりしても、影響を受けるのはその 1 つのリクエストだけです。サーバーは他のすべてのリクエストを通常どおり処理し続けます。クラッシュしたワーカーは、自動的に新しいものに置き換えられます。
  • ワーカーの自動再生成。 OxPHP はすべての PHP ワーカーの健全性を監視します。ワーカーが予期せず停止した場合、手動での介入なしに、その場所に新しいワーカーが起動されます。
  • バックプレッシャー保護。 有界リクエストキューが過負荷を防ぎます。サーバーが容量に達すると、新しいリクエストは、無期限にキューイングされて連鎖的なタイムアウトを引き起こす代わりに、Retry-After ヘッダー付きの 529 レスポンスを受け取ります。
  • パストラバーサル保護。 すべての URL パスは、ファイルシステムへのアクセス前にサニタイズされます。パーセントエンコードされたトラバーサルの試み、.. セグメント、およびドキュメントルートを抜け出すパスはブロックされます。
  • グレースフルシャットダウン。 SIGTERM または SIGINT(Ctrl+C)を受け取ると、OxPHP は新しい接続の受け付けを停止し、終了する前に処理中のリクエストの完了を待ちます(設定可能なドレインタイムアウトまで)。

関連項目