ワーカーモード
ワーカーモードは、一度だけブートストラップしてその後多数のリクエストを処理する永続的な PHP プロセスを実行します。そのため、PHP の起動コストはリクエストごとにではなく一度だけ支払われます。リクエストごとに PHP の状態を破棄して再構築するのではなく、アプリケーションはオートローダー、設定、データベース接続を一度だけ読み込み、ワーカーの生存期間を通じてそれらを再利用します。
仕組み
- ワーカーモードを有効化します。
WORKER_MODE_ENABLED=trueを設定し、ENTRY_FILEをブートストラップスクリプトに向けます。これにより、プール内のすべての PHP ワーカーでワーカーモードが有効になります。 - 一度だけブートストラップします。 PHP が起動し、外側のスコープを一度だけ実行します。オートローダーの登録、設定の読み込み、データベース接続、その他の初期化コードは一度だけ実行されます。
- リクエストループに入ります。
oxphp_worker(callback)を呼び出します。OxPHP は受信した HTTP リクエストをコールバックへディスパッチし始めます。 - リクエスト間でリセットします。 スーパーグローバル(
$_GET、$_POST、$_SERVER、$_COOKIE、$_FILES、$_REQUEST、php://input)、出力バッファ、レスポンスヘッダー、そしてリクエストが変更した ini ディレクティブは自動的にリセットされます。何がカバーされ、どこで止まるかについてはリセットされるものと維持されるものを参照してください。ソフトリセットは、外側のスコープでブートストラップされたリソースを保持しつつ、リクエストごとの状態をクリーンアップします。$_ENVは例外で、意図的にリセットされません — スーパーグローバルを参照してください。 - 外側のスコープは維持されます。
oxphp_worker()より前に定義された変数、静的プロパティ、データベース接続、オートローダーは、そのワーカーが処理するすべてのリクエストにわたって利用可能なまま残ります。
ワーカーモードはルーティングの挙動を変更します。ディスク上の静的ファイルにマッチしないすべてのリクエストは、404 を返す代わりにワーカーへディスパッチされます。詳細は ルーティング を参照してください。
設定
| Variable | Default | Description |
|---|---|---|
WORKER_MODE_ENABLED |
false |
永続的なワーカーモードを有効にします。true、1、yes を受け付けます。ENTRY_FILE が .php スクリプトを指している必要があります |
ENTRY_FILE |
(未設定) | ワーカーのブートストラップスクリプトへのパス。相対パスの場合は DOCUMENT_ROOT を基準に解決されます。.. セグメントと絶対パスが許可されます(公開ドキュメントルートの外に置かれたワーカーブートストラップはサポートされているレイアウトです) |
WORKER_MAX_MEMORY_MIB |
0 |
リサイクル前のワーカーごとの最大 PHP メモリ量(MiB 単位)。0 = 無制限 |
レガシーの WORKER_FILE 変数は引き続きパースされ(起動時に WARN が出ます)、WORKER_MODE_ENABLED=true ENTRY_FILE=$WORKER_FILE と同じように振る舞います。新しいデプロイでは明示的なペアを使用してください。レガシーの形式は将来のリリースで削除されます。
アプリケーション駆動のリサイクルには、リクエストハンドラーの内部から OxPHP\Server\Worker::scheduleExit() を呼び出します。ワーカーは現在のリクエストが完了した後にクリーンに終了します。
ワーカースクリプトの記述
ワーカースクリプトは 2 つの部分から構成されます。起動時に一度だけ実行される外側のスコープと、リクエストごとに実行される oxphp_worker() に渡されるコールバックです。
<?php
// Outer scope: runs once at startup
require __DIR__ . '/../vendor/autoload.php';
$config = parse_ini_file(__DIR__ . '/../config/app.ini');
$db = new PDO($config['dsn'], $config['user'], $config['pass'], [
PDO::ATTR_PERSISTENT => true,
]);
$app = new MyApp\Application($config, $db);
// Request loop: runs for every request
oxphp_worker(function () use ($app) {
$app->handle();
});
// Shutdown: runs when the worker exits
$app->terminate();リセットされるものと維持されるもの
OxPHP はリクエスト間でソフトリセットを実行します。リクエストごとの状態は自動的にクリーンアップされ、外側のスコープでブートストラップされたものはワーカーの生存期間を通じて残ります。
- スーパーグローバル —
$_GET、$_POST、$_SERVER、$_COOKIE、$_FILES、php://inputは新しいリクエストデータで再投入されます - 出力バッファ — すべての出力バッファがフラッシュされ、クリーンアップされます
- レスポンスヘッダー — HTTP ステータスコードとヘッダーはデフォルトにリセットされます
- エラー状態 — 直近のエラー情報(メッセージ、ファイル、行、タイプ)と接続ステータスがクリアされます。ユーザーが登録したエラーハンドラー(
set_error_handler())と例外ハンドラー(set_exception_handler())はリクエスト間で維持されます - リクエストが変更した ini ディレクティブ —
ini_set()、set_time_limit()、error_reporting()は、ワーカーの次のリクエストの前にブートストラップ時のベースラインへロールバックされます。境界については後述します
- 外側のスコープの変数 —
oxphp_worker()より前に定義され、useでキャプチャされたすべてのもの - 静的プロパティ — クラスの静的プロパティはその値を保持します
- データベース接続 — PDO、MySQLi、その他の永続的な接続は開いたままになります
- オートローダー — 登録されたオートローダー(Composer、カスタム)は有効なまま残ります
- 読み込み済みのクラスと関数 — 以前に読み込まれたすべてのクラス、インターフェース、トレイト、関数
- ブートストラップで設定された ini ディレクティブ — 外側のスコープでの
ini_set()はワーカーの生存期間を通じて保持され、リクエストごとの変更がロールバックする先になります
ini のロールバック
リクエストが ini_set()、set_time_limit()、error_reporting() で変更したものは、PHP-FPM 下と同じように、ワーカーが単独で次のリクエストを受け取る前に元へ戻されます。ある HTTP 呼び出しを囲む ini_set('default_socket_timeout', 5) も、バックグラウンド処理の分岐での set_time_limit(0) も、それを行ったリクエストにのみ適用され、次のリクエストには適用されません。復元先のベースラインは php.ini の値ではなく、ブートストラップが設定したものです。外側のスコープでの ini_set() はアプリケーション設定であり、すべてのリクエストを生き延びます。知っておく価値のある境界が 3 つあります。
- ロールバックは、ワーカーが他に何も実行していない状態で次のリクエストを受け取るときに行われます。 ini ディレクティブはリクエストではなくワーカースレッドに属します。複数のリクエストを同時に処理しているワーカー — リクエストが await、スリープ、ソケット読み取りで一時停止しているときは常にそうなり、ファイア・アンド・フォーゲットの Promise がまだ回収されていない間も同様です — は、まだ実行中の別のリクエストから奪うことなしに、あるリクエストの変更を元に戻すことはできません。そして、それを試みません。ワーカーに処理中の作業がある間、そのワーカー上で行われた変更は、その期間に受け取るリクエストからも見えたままです。リクエストを並行処理するアプリケーションでは、
display_errorsのような漏れが問題になるディレクティブの封じ込めをロールバックに頼らないでください。 memory_limitは、リミットとして復元される前に、まず値として復元されます。 PHP は、新しい上限を超えるメモリがまだ保持されている間、アロケーターの上限を下げることを拒否します。そのため、リクエストが割り当てたものを抱えたままのワーカーは、ini_get()では復元後のmemory_limitを報告する一方で、アロケーターは引き上げられた方を強制し続けます。ワーカー自身のフットプリントに余裕ができ次第、上限も追従します。opcache.enableは一切復元されません。 リクエストが OPcache に対してできるのはオフにすることだけであり(PHP はリクエストの途中で再びオンに切り替えることを拒否します)、それを再び立ち上げるのは OPcache 自身のリクエストごとの起動処理ですが、ワーカーはそれを起動時に一度しか実行しません。したがって、リクエストが OPcache をオフにしたワーカーは、その後の生存期間中すべてのファイルをソースからコンパイルし続け、ディレクティブはそれを示すために0と読めるまま残されます。復元してしまうと、ini_get('opcache.enable')とopcache_get_status()が、動いていないキャッシュを有効と報告することになります — 何かを判断するために問い合わせるアプリケーションが、実際に起きていることと逆を告げられることになるため、そちらの方が悪い選択です。
リサイクル
ワーカーは、以下のいずれかの条件が満たされると自動的にリサイクルされます(新しい PHP プロセスで再起動されます)。
- 最大メモリ超過 — ワーカーの PHP メモリ使用量が
WORKER_MAX_MEMORY_MIBMiB を超えた場合 - アプリケーションによる終了要求 — ハンドラーが
Worker::scheduleExit()を呼び出した場合。アプリケーション制御のホットリロード、ファイルの mtime に基づくリロード、リクエストごとのブートストラップ再実行などに役立ちます - 連続エラー — ワーカーが 3 回連続で崩壊したリクエストを受け取った場合。ここで言う崩壊とは、致命的エラー、メモリ不足、スタックオーバーフローです。これらが残すのは次のリクエストが引き継ぐことになるエンジン状態であり、リサイクルはまさにそのためにあります。何がカウントされ、何がカウントされないかは以下を参照してください
失敗したリクエストのすべてがカウントされるわけではありません。すべての失敗が、ワーカーが処理に不適格になったことを意味するわけではないからです。
| 結果 | カウントへの影響 |
|---|---|
| 致命的エラー、メモリ不足、スタックオーバーフロー — どこで発生しても同じで、リクエスト終了時に実行されるシャットダウン関数やデストラクタの中も含まれます | カウントされる |
未捕捉の例外(500 で応答)— リクエストハンドラーからでも、シャットダウン関数からでも |
中立 |
キャンセルされたリクエスト — クライアントが切断した、max_execution_time が経過した、サーバーがシャットダウン中 |
中立 |
リクエストの完了(exit()/die() を含む) |
カウントをクリアする |
「中立」とは文字どおりの意味です。致命的エラーの連続の途中にこれらのいずれかが挟まっても、カウントを増やすこともクリアすることもないため、fatal, exception, fatal, fatal でもワーカーはリサイクルされます。失敗がどこで発生したかは、その読み取り方に影響しません。PHP はシャットダウン関数を独自の保護下で実行するため、ワーカーからはその中で失敗したリクエストも正常に戻ってきたように見えます — しかし、そこでの致命的エラーは、他のあらゆる致命的エラーと同じく、そのワーカー上の次のリクエストが引き継ぐ残骸を残すため同じようにカウントされ、そこでの例外はハンドラーからの例外と同じくクリーンに巻き戻るため同じように中立です。着弾のタイミングによって読み方が変わる唯一のものがデッドラインです。シャットダウン関数の実行中に期限が切れた場合も、それはサーバーがリクエストを終わらせているだけなので中立のままですが、その前にリクエストが自力で失敗していた場合は、カウントされたままになります。これらのいずれも行わないのは、失敗したのではなく固まってしまったワーカーの診断です。システムコールで動けなくなったリクエストは、キャンセルもカウントもされず、オペレーターが対処できるように oxphp_worker_stuck_total を通じて報告されます。
ワーカーがリサイクルされると、PHP プロセスは終了し、新しいプロセスが起動して、ワーカースクリプトの外側のスコープを再実行します。メモリベースおよびスケジュールされた終了の場合、ワーカーが終了する前に現在のリクエストが正常に完了します。エラーベースのリサイクルの場合、ワーカーは失敗したリクエストの後に終了します。
同じワーカーが並行して処理していた他のリクエスト — oxphp_async_await() や oxphp_sleep()、あるいは RUNTIME_HOOKS 下のソケット読み取りで中断していたもの — は、最後まで実行されません。それぞれ中断していた場所でキャンセルされ、自身のシャットダウン関数を実行した後、Retry-After 付きの 503 Service Unavailable で応答されます。したがってリサイクルは、たまたま処理中だったリクエストのクライアントから見えるものであり、並行リクエストを処理するワーカーで WORKER_MAX_MEMORY_MIB を選んだり scheduleExit() を呼び出したりする際に知っておく価値があります。サーバー全体のシャットダウンは異なります。そこでは、処理中のリクエストはドレイン期間を与えられ、正常に完了できます。
開発時のリロード
ワーカーモードはブートストラップの状態(オートローダー、DI コンテナ、DB 接続)をメモリ内に保持するため、外側のスコープで実行されたコードの変更を反映するには opcache.validate_timestamps=1 だけでは不十分です。開発時のループには 2 つの選択肢があります。
- リクエストごとにリサイクルする。 すべてのハンドラー呼び出しの最後に
OxPHP\Server\Worker::current()->scheduleExit()を呼び出します(たとえばOXPHP_DEV環境変数フラグで制御します)。現在のリクエストは正常に完了し、その後ワーカーは終了して再生成され、外側のスコープが再実行されます。これはワーカーモードのパフォーマンス上の利点を FPM スタイルのリロードセマンティクスと引き換えにします。アクティブな開発において最もシンプルで信頼性の高いアプローチです。 - ワーカーをウォームに保ち、リクエストハンドラーをリロードする。
scheduleExit()を完全にスキップし、opcache.validate_timestamps=1を有効にして、ブートストラップを最小限に保ちます。リクエストコールバック内で読み込まれたコードは、次のリクエストで OPcache によってリフレッシュされます。外側のスコープで一度だけ読み込まれたコードはリフレッシュされません。注意点の完全な一覧については OPcache と JIT → 開発時の設定 を参照してください。
トラブルシューティング
リクエストがハングして完了しない
ブートストラップスクリプトで oxphp_worker() が一度も呼び出されないと、リクエストはディスパッチされず、すべてのリクエストが無期限に待機します。スクリプトが通常のコードパスで無条件に oxphp_worker() を呼び出していることを確認してください。
リクエスト間で状態がリークする
oxphp_worker() コールバック内で定義された変数は PHP のガベージコレクターによってクリーンアップされますが、外側のスコープで定義された静的プロパティやグローバル変数は維持されます。あるリクエストのデータが別のリクエストに現れる場合は、呼び出しをまたいで状態を蓄積している静的プロパティやグローバル変数がないか確認してください。
対処法: 各リクエストコールバックの先頭で静的状態を明示的にリセットするか、リクエストごとの状態を静的変数に格納しないようにします。
ワーカーが即座にリサイクルされる(メモリ制限)
ワーカーのメモリ制限は、PHP が報告するメモリ使用量を用いて各リクエストの後にチェックされます。ブートストラップフェーズで大量のメモリを割り当てる場合(たとえば大きなキャッシュを読み込む場合)、初期のメモリフットプリントがすでに制限に近い可能性があります。
対処法: WORKER_MAX_MEMORY_MIB を増やすか、大きな割り当てを最初のリクエストまで遅延させます。
ワーカーが即座にリサイクルされる(エラー制限)
3 回連続の致命的エラーでリサイクルが発生します。リクエストコールバック内の致命的エラーをアプリケーションのログで確認してください — 未捕捉の例外やキャンセルされたリクエストはカウントされないため、探すべきはそれらではありません。
確認: アクセスログまたは構造化ログ出力でエラーを探します。
docker logs <container> 2>&1 | grep '"level":"error"'アイドル後にデータベース接続が切断される
データベースサーバーがアイドル接続を閉じる場合、次のリクエストでの再接続の試みが失敗する可能性があります。再接続を処理するコネクションプールを使用するか、例外をキャッチして手動で再接続してください。
Docker の例
services:
app:
image: ghcr.io/oxphp/oxphp:0.11.0
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
environment:
- DOCUMENT_ROOT=/var/www/html/public
- WORKER_MODE_ENABLED=true
- ENTRY_FILE=/var/www/html/worker.php
- WORKER_MAX_MEMORY_MIB=128PHP API
ワーカーのイントロスペクションとワーカーのエントリーポイントは、OxPHP\Server\Worker クラスを通じて公開されます。
<?php
$worker = OxPHP\Server\Worker::current();
$worker->serve(function () {
handleRequest();
});レガシーの自由関数(oxphp_is_worker、oxphp_worker_id、oxphp_worker)は引き続き利用可能で、同じ内部状態を経由します。新しいコードではクラス API を優先してください。
このクラスは、グレースフルな自己リサイクル、可観測性、ヘルスチェックに役立つランタイムのイントロスペクションも公開します。
| Method | Returns |
|---|---|
Worker::isWorkerMode(): bool |
サーバーがワーカーモードで実行されているかどうか |
$worker->id(): int |
スレッドごとの安定したワーカーID |
$worker->startTime(): float |
このワーカーが起動した Unix タイムスタンプ |
$worker->requestCount(): int |
このワーカーが処理したリクエスト数 |
$worker->memoryUsage(): int |
このワーカーの現在の memory_get_usage(true) |
$worker->rss(): int |
現在の resident set size(バイト単位、Linux/macOS) |
$worker->maxMemoryBytes(): int |
リサイクルのしきい値 — WORKER_MAX_MEMORY_MIB × 1 MiB、無制限の場合は 0 |
$worker->isExitScheduled(): bool |
scheduleExit() が呼び出されたかどうか |
$worker->exitReason(): ?string |
実行中は null。ワーカーが停止する際は "scheduled"、"max_memory"、"error" のいずれか |
完全なシグネチャと実践的な例については OxPHP\Server\Worker を参照してください。
PHP の例
ワーカーモードの検出
現在のプロセスがワーカーモードで実行されているかどうかを確認するには、OxPHP\Server\Worker::isWorkerMode() を使用します。これは、従来のモードとワーカーモードの両方で動作するコードを書く際に役立ちます。
<?php
if (OxPHP\Server\Worker::isWorkerMode()) {
// Reuse a persistent connection
$redis = new Redis();
$redis->pconnect('redis', 6379);
} else {
// Traditional mode: connect per request
$redis = new Redis();
$redis->connect('redis', 6379);
}Symfony のワーカースクリプト
<?php
use App\Kernel;
require __DIR__ . '/../vendor/autoload.php';
$kernel = new Kernel('prod', false);
$kernel->boot();
oxphp_worker(function () use ($kernel) {
$request = Symfony\Component\HttpFoundation\Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
});
$kernel->shutdown();ベストプラクティス
WORKER_MAX_MEMORY_MIBを設定します(たとえば128)。これにより、リークしているワーカーがホストを消費し尽くす代わりに自動的にリサイクルされます。さらにアプリケーション駆動のリサイクルのためにWorker::scheduleExit()と組み合わせます。- リクエストごとの状態を静的プロパティやグローバル変数に格納しないようにします。 これらはリクエスト間で維持されるため、あるリクエストの残留状態が別のリクエストにリークする可能性があります。
- ソフトリセットを早い段階で検証します。 開発用フラグの下でハンドラーに
Worker::current()->scheduleExit()を追加し、アプリケーションをエンドツーエンドで動かしてみます。これにより、長寿命のワーカーに移行する前に状態リークのバグを捕捉できます。 - データベースのアイドルタイムアウトに対処します。 データベースドライバーがアイドル期間の後に切断する場合は、例外をキャッチして再接続するか、再接続を自動的に処理するコネクションプールを使用します。
- 外側のスコープを最小限に保ちます。 本当に維持する必要があるもの、つまりオートローダー、設定、共有サービスだけをブートストラップします。リクエスト固有のセットアップはコールバックに遅延させます。