設定リファレンス

OxPHP は環境変数と、オプションのファイル 1 つで設定します。そのファイルは .oxphpdeny で、DOCUMENT_ROOT の最上位から読み込まれるパスの拒否ルールです。すべての設定項目にデフォルト値があるため、設定ゼロのデプロイがそのまま動作します。

ブール値

ブール値として指定された変数は、大文字・小文字を区別せず前後の空白を取り除いたうえで、決まった正規の値の集合を受け付けます。

  • 真値: on, true, 1, yes
  • 偽値: off, false, 0, no

その集合に含まれない空でない値、たとえば ture のようなタイプミスは、起動時に該当する変数名を示すエラーで即座に失敗します。これにより、フラグが誤った向きに静かに切り替わってしまう前に、トラフィックが流れ始める前の段階で設定ミスを検出できます。

未設定の変数や空の代入(FOO=)は、ドキュメントに記載されたデフォルト値にフォールバックします。空を未設定として扱うのは意図的な挙動です。Docker Compose / Kubernetes の FOO=${FOO} のような変数展開は、ホスト側の変数が存在しないときに FOO= を生成しますが、それによってサーバーの起動が拒否されるべきではないからです。

サーバー

変数 デフォルト 説明
LISTEN_ADDR 0.0.0.0:80 メイン HTTP サーバーのアドレスとポート
DOCUMENT_ROOT /var/www/html/public ファイルと PHP スクリプトを配信するルートディレクトリ
ENTRY_FILE (未設定) 単一の正規エントリースクリプト。未設定 = 直接ファイルマッピング。*.php = フロントコントローラー。.php 以外 = 静的フォールバック(SPA)。WORKER_MODE_ENABLED=true の場合 = ワーカーのブートストラップ。DOCUMENT_ROOT を基準に解決されます(相対パスと .. は許可、絶対パスはそのまま使用)。ルーティングを参照
WORKER_MODE_ENABLED false 永続的なワーカーモードを有効化します。ENTRY_FILE が .php スクリプトを指している必要があります。ブール値 — ブール値を参照
MAX_CONNECTIONS 10000 同時 TCP 接続の最大数。デフォルトの QUEUE_MAX_WAITING の上限(その半分)でもあるため、不正な形式の値は静かなフォールバックではなく起動エラーになります。これを下げても QUEUE_CAPACITY のデフォルトは動きません。そちらはワーカー数だけから決まるためです — PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING より大きく保ってください。accept ループのための余裕を確保するを参照
TOKIO_WORKERS CPU / 2(最小 1) 非同期 I/O スレッド。1 = シングルスレッド、N > 1 = スレッド数固定、未設定 = 自動(CPU / 2、最小 1)。非同期ランタイムのサイズを決めるを参照

非同期ランタイムのサイズを決める

TOKIO_WORKERS のデフォルトは利用可能なコア数の半分ですが、残りの半分は遊んでいるわけではありません。それは PHP ワーカープールのものです。2 組のスレッドは同じコアを奪い合うため、アプリケーションからサイクルを奪わずにランタイムだけを広げることはできません。

プールが既にコア数近くにサイズ設定されている環境では、これをコア数いっぱいまで引き上げるのは損になります。4 コアのマシンで、圧縮されたレスポンスを返す 4 ワーカーのプールで測定したところ、ランタイムスレッドを 2 から 4 に増やすと、1.6 KB の JSON ボディでレートが 13%、5 KB のボディで 26% 低下し、しかもそのために多くの CPU を消費しました。スレッドがオーバーサブスクライブされていたためです。

バランスが異なる環境では引き上げてください。マシンに対してプールが小さい場合や、処理が PHP バウンドではなく I/O バウンド(プロキシされたアップストリーム、静的ファイル、ストリーミングレスポンス)で、ランタイムが仕事をしてプールはほとんど待っているだけの場合です。変更するときは必ず、レートと合わせてプールも見てください。oxphp_busy_workers と oxphp_workers_current を比べれば、コアがどちら側に回ったかが分かります。プールがより忙しくなりながらレートが上がるなら本物の改善です。プールが空いていきながらレートが上がる場合は、たいてい処理量が増えたのではなく、仕事が移動しただけです。

Tip

TOKIO_WORKERS と PHP_WORKERS はどちらもデフォルトで利用可能なコア数の半分なので、その合計はちょうどコア数になります。片方を変えれば合計も動きます。だからこそ、どちらも推測ではなく測定する価値があるのです。

PHP ワーカー

変数 デフォルト 説明
EXECUTOR sapi PHP エグゼキューターのバックエンド。sapi は PHP の実行、stub は PHP なしでのベンチマーク用
PHP_WORKERS CPU / 2(最小 1) ワーカープールのサイズ。N = 固定プール、MIN:MAX = 動的スケーリング、0 = 自動
PHP_WORKERS_IDLE_SECONDS 30 動的ワーカーが破棄されるまでアイドル状態を保つ秒数(動的モードのみ)。ワーカーは処理中のものを何も持たない瞬間に破棄されるため、処理中のリクエストが途中で切られることはありません — また、終わらないリクエスト(開いたままのストリームなど)を抱えたワーカーはそのような瞬間に決して到達せず、まったく破棄されません
QUEUE_CAPACITY 初期ワーカー数 × 128 PHP キューで保留できるリクエストの最大数。満杯のキューに到着したリクエストはスロットの空きを待ちます(QUEUE_WAIT_TIMEOUT_MS を参照)。動的プール(MIN:MAX)の場合、初期ワーカー数 = 最小値。0 = 自動
QUEUE_WAIT_TIMEOUT_MS 1000 リクエストが 529 で拒否されるまでに PHP ワーカーを待てる最長の時間。これは固定値ではなく上限です。到着時に刻印される単一の期限で、リクエストが直面しうる両方の待機 — キューのスロット待ちと、キュー内でのワーカー待ち — をカバーします。過負荷時にはサーバーが自ら予算をこの上限より短くし、キューが空になれば延ばし戻します。oxphp_admission_wait_budget_us は現在有効な値を、oxphp_admission_wait_budget_ceiling_us は設定された値を公開します(適応的な待機予算を参照)。期限は両端から強制されます。期限を過ぎたリクエストには 529 で応答し、期限を過ぎたリクエストに到達したワーカーはそれを実行せずに拒否するため、誰にも取り出されないキュー内のリクエストがいつまでも待たされることはありません。その後ハンドラーがどれだけ実行されるかは制限しません。同時に待機できるのは最大 QUEUE_MAX_WAITING 件で、それを超えたリクエストは即座に拒否されます。0 = キューが満杯なら即座に拒否。これはキュースロットの待機をなくしますが、キュー内での待機はなくしません。アプリケーションが HTTP 経由でこの同じサーバーを呼び返す場合(内側の呼び出しは外側の呼び出しが保持しているワーカーを必要とするため、待機が無駄になります)や、前段のロードバランサーがより短い独自のタイムアウトを持つ場合は、これを下げてください。待機の途中で接続を閉じたクライアントは、HTTP/1.1 でも HTTP/2 でも、即座にその場所を返します。既にキューに入った後で接続を閉じたクライアントは、ワーカーが到達するか予算が尽きるまで場所を保持し、そのうえで過負荷による拒否として数えられるのではなく 499 で応答されます。サーバーに見えないのは、接続を閉じずに待つのをやめたクライアントです。そのようなクライアントは受け入れられるか予算が尽きるまで場所を保持し続け、先にワーカーが空けばそのスクリプトは誰のためでもなく実行されます。したがって、予算を前段にあるもののタイムアウトより短く保つことには、依然として価値があります
QUEUE_MAX_WAITING 初期ワーカー数 × 128、上限は MAX_CONNECTIONS / 2 キュースロットを待って同時に待機できるリクエストの最大数。これを超えたリクエストは待機せず即座に拒否されます。各待機者は、待機している間ずっと接続と完全にバッファ済みのリクエストボディを保持するため、これは報われる見込みのある待機ではなく、保持されるリソースに対する制限です。デフォルトに適用される MAX_CONNECTIONS / 2 の上限が制限するのはバックログのこの部分だけです。キュー内および実行中のリクエストも同じように接続を保持するため、サーバーが受け入れと拒否のために実際に確保している余裕は、MAX_CONNECTIONS に対する PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING から導かれます — 後述を参照。サイズは自身のハンドラーレイテンシから決めてください — こちらも後述。0 = 自動、1 未満にはなりません
QUEUE_MAX_WAITING_BYTES 67108864(64 MiB) 待機中のリクエストが合計で保持できるリクエストボディの最大バイト数。ボディが合計をこれより先へ押し上げるようなリクエストは、待機せず即座に 529 で拒否されます。ボディを持たないリクエストがこれを理由に拒否されることはありません。QUEUE_MAX_WAITING は同じセットをリクエスト数で制限しますが、それはサイズについて何も語りません — ボディはリクエストがキューに到達する前に完全にバッファされるため、数だけで止めるなら待機セットは QUEUE_MAX_WAITING × 10 MiB を抱えることができてしまいます。大きなボディのバーストを切り捨てるのではなく吸収すべきアップロード中心のアプリケーションでは引き上げ、メモリ上限のあるコンテナでは引き下げてください。0 = 自動

待機セットのサイズを決める

有意義に待機できるリクエストの数は、プールがどれだけ速くそれらを処理し終えるかから導かれます。W 個のワーカーとハンドラーの所要時間 T ミリ秒なら、プールは 1 ミリ秒あたり W / T 件のリクエストを受け入れられるため、B ミリ秒の予算はおよそ W × B / T 件の待機者を入れられます。それを超えたものはすべて、予算をまるごと待った挙げ句に拒否され、その間ずっと接続とバッファ済みのリクエストボディを保持します。

サービスレートは起動時には分からないため、デフォルト値はこれを計算できず、意図的に寛大になっています。これは速いハンドラーには合っています。プールが深いバックログを予算内で余裕をもって掃けるからです。一方、遅いハンドラーにははるかに大きすぎます。8 ワーカー、200 ミリ秒のハンドラー、デフォルトの 1 秒の上限(プールが負荷を切り捨てていないときに有効な予算)では、時間内に受け入れられるのは約 40 件だけなのに、デフォルトは最大 1024 件を待機させます。

bash
QUEUE_MAX_WAITING=40

これを W × B / T の近くに設定すると、余剰分は 1 秒後の 529 ではなく即時の 529 になります。QUEUE_WAIT_TIMEOUT_MS を短くしても、もう一方の項によって同じことが達成できます。両者はトレードオフの関係にあり、遅いハンドラーでは通常、予算を小さくする方が良いレバーです。クライアントが拒否を待つ時間も制限するからです。ここでの B は現在有効な予算であり、過負荷時には設定された上限ではなく、サーバーが短縮した後の値です。設定ファイル上の数値から待機セットのサイズを決める前に、oxphp_admission_wait_budget_us を読んでください。

このセットはバイト単位でも重ねて制限されます。各待機者は待機している間ずっとリクエストボディをメモリに保持するため、リクエスト数で数える上限は、保持されるメモリをトラフィック任せにします。同じ 1024 件の待機者でも、ボディのない GET ではコストはゼロ、アップロードではギガバイト単位です。QUEUE_MAX_WAITING_BYTES はその合計を直接制限します — これを超えると、ボディを持つリクエストは待機させられるのではなく即座に拒否され、ボディのないリクエストは通常どおり待機を続けます。バーストを切り捨てるのではなく吸収すべきアップロード中心のアプリケーションはより大きな値を、メモリ上限が固いコンテナはより小さな値を求めます。どちらの上限による拒否も oxphp_admission_refused_total で別々にカウントされる(数に対しては waiting_full、メモリに対しては waiting_bytes)ため、メトリクスがどのノブに手を伸ばすべきかを教えてくれます。

適応的な待機予算

QUEUE_WAIT_TIMEOUT_MS は上限です。待機によって受け入れられた仕事が、既に去ったクライアントのために完了している間、サーバーは自ら、しかも急激に予算を短縮します。上限を選ぶ前に知っておく価値があります。

  • 下げるとき: プールが開始したものの 4 分の 1 以上が既に去ったクライアント向けだったたびに、予算は半分になります。測定は固定の間隔ではなく、少なくとも 8 件の開始済みリクエストからなるウィンドウで行われます。
  • 下限: 上限の 64 分の 1(デフォルトでは 15.6 ミリ秒)と 10 ミリ秒のうち大きい方。ただし上限そのものを超えることはありません。
  • 上げるとき: 上限の 8 分の 1 ずつ戻っていきます。ただし、キューが空であると観測され、かつプールが開始したもののうち去ったクライアント向けが 4 分の 1 未満である間に限ります。閉じられたタブやクライアントのタイムアウトといった普段の少量の離脱が予算を押し下げたままにすることはなく、キューが決して空にならないサーバーは、負荷が和らぐまで短縮された予算のままです。

1000 を設定したオペレーターは、エピソードの最中には約 15 が有効になっていると想定すべきです。したがって、待機に関するアラートは設定値ではなく、oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us のペアに対して書いてください。

短縮を駆動するのは、接続を閉じるクライアントです。閉じずに待つのをやめる集団はシグナルを生まず、上限が有効なまま残ります。

自己呼び出し

HTTP 経由でこの同じサーバーを呼び返すアプリケーションは、待機の長さにかかわらずそれを無駄にします。内側の呼び出しは、外側の呼び出しが保持しているワーカーを必要とするからです。oxphp_admission_wait_wasted_total はそうした待機を数えますが、予算が上限にある間だけです。予算が下がっている間はこのカウンターは沈黙するため、そこが 0 であることだけでは、そのパターンが存在しないことの証明になりません。予算が下がるかどうかも自己呼び出しの性質ではありません。自身の呼び出し元が寛大に待つ(内部の cron、長いタイムアウトを持つサイドカーなど)自己呼び出し元は、上限を保ちます。

QUEUE_WAIT_TIMEOUT_MS=0 はこれを解決しません。これはキュースロットの待機をなくしますが、キュー内での待機はなくしません。そのため、キューが満杯になるほど QUEUE_CAPACITY が小さくない限り、内側の呼び出しは誰も消費していないキューに受け入れられ、決して応答されません。

accept ループのための余裕を確保する

リクエストは、到着した瞬間から応答されるまで、その間に何をしていようと、接続 — そして MAX_CONNECTIONS のパーミットの 1 つ — を保持します。キュースロットはワーカーがリクエストを取り出した瞬間、スクリプトが実行される前に解放されるため、これは 3 つの別々の集団をカバーします。

  • ワーカー内で実行中: 少なくとも PHP_WORKERS 件。ワーカーモードでは 1 つのスレッドがファイバーを多重化するため、それ以上になります。
  • キュー内: 最大 QUEUE_CAPACITY 件。
  • 受け入れ待ちで待機中: 最大 QUEUE_MAX_WAITING 件。

MAX_CONNECTIONS に由来する上限を持つのは 3 番目だけです。PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING は MAX_CONNECTIONS を下回るように保ってください。これを超えると、障害モードはより悪い方へ変わります。accept ループは接続の処理を始める前に接続パーミットを取得するため、PHP のパスがすべてを保持してしまうと accept ループは停止し、その時点で到着したクライアントは 529 の代わりにまったく応答を得られません — ロードバランサーはこれを死んだインスタンスと見分けられず、一方 INTERNAL_ADDR 上のヘルスプローブはどちらの制限も経由しないため、グリーンのままです。

この停止はログに記録されます。パーミットを待たなければならない accept は予算を名指しする WARN を記録し、パーミットが届いた瞬間に、ループがどれだけ停止していたかを報告する INFO が書かれます。報告はレート制限されているため、上限付近で揺れ動くサーバーが自分のログで障害を引き起こすことはありません。報告済みの停止から数秒以内に始まった停止は最初は保留され、そのウィンドウが閉じた時点でまだ続いていれば報告されます。ウィンドウ内で終わった停止は 1 行も残しません。メトリクスはすべてを捉えます。oxphp_accept_stalled(ゲージ。内部リスナーは MAX_CONNECTIONS を経由しないため、停止が続いている間も読み取れます)と oxphp_accept_stalls_total(カウンター)です。メトリクスリファレンスを参照してください。到着したクライアントは依然としてエラーではなく無応答を受け取りますが、ログとメトリクスには停止が記録されます。

サーバーは起動時にこれをチェックし、合計が予算に達すると、すべての値を挙げて警告します。oxphp config --check も同じものを報告しますが、判定結果や終了コードは変わりません。警告の解消は十分条件ではなく必要条件として扱ってください。アイドル状態の keep-alive 接続、静的ファイルのリクエスト、進行中のハンドシェイクもパーミットを保持しますが、いずれも起動時には数えられないからです。

この警告への通例の入り口は MAX_CONNECTIONS を下げることです。QUEUE_CAPACITY のデフォルトはワーカー数から決まり、それに追従して下がらないためです — 7 ワーカーと MAX_CONNECTIONS=1000 では、予算 1000 に対して 7 + 896 + 500 になります。どのノブに手を伸ばすかに注意してください。QUEUE_MAX_WAITING をデフォルトのままにして MAX_CONNECTIONS だけを引き上げても警告は解消されません。そのデフォルトは MAX_CONNECTIONS の半分であり、一緒に上がっていくからです — 7 ワーカーでは合計は 1799 に落ち着き、条件は MAX_CONNECTIONS=1800 以上でようやく解消されます。QUEUE_CAPACITY を下げるか、QUEUE_MAX_WAITING を明示的に設定したうえで予算を引き上げてください。

動的プール(MIN:MAX)では、実行中の項は最小値 — キューのデフォルトの算出元と同じ数 — なので、最大まで成長したプールは合計が示すより多くを保持します。

この比較は接続数を数えますが、予算はリクエストによって消費されます。そのため HTTP/2 中心のデプロイメントは、正当にこの合計を上回ることがあります。1 つの接続は最大 H2_MAX_CONCURRENT_STREAMS 件のリクエストを運ぶため、同じバックログが接続のごく一部によって保持されるからです。そこではこの警告は想定内です。標準の予算のままの大きなプールも警告に達します — 自動サイズの 39 ワーカーのプールは 10 000 に対して 39 + 4992 + 4992 になります — こちらは無視するのではなく、対処する価値があります。

静的ワーカーと動的ワーカー

固定プールにするには、PHP_WORKERS に単一の数値を設定します。

bash
PHP_WORKERS=8 # Fixed 8 workers PHP_WORKERS=0 # Auto-detect: CPU / 2 (min 1)

自動スケーリングにするには、PHP_WORKERS に MIN:MAX を設定します。

bash
PHP_WORKERS=2:16 # Scale between 2 and 16 workers PHP_WORKERS=4:0 # 4 minimum, auto-detect maximum (CPU × 2) PHP_WORKERS=0:16 # auto-detect minimum (CPU / 4, min 1), 16 maximum

動的モードでは、OxPHP はすべてのワーカーがビジー状態のときにワーカーをスケールアップし、ワーカーが PHP_WORKERS_IDLE_SECONDS より長くアイドル状態だったときにスケールダウンします。

CPU 数の検出方法

このページで CPU を基準に記述されているデフォルト値(TOKIO_WORKERS、および未設定・0・MIN:MAX のいずれかの側が 0 の PHP_WORKERS)はすべて、プロセスが実行を許可されている CPU を数えます。基準になるのはプロセスのアフィニティマスク(taskset、docker run --cpuset-cpus)で、コンテナに CPU クォータが設定されている場合はそこまで引き下げられます(docker run --cpus。cgroup v2 では cpu.max、v1 では cpu.cfs_quota_us)。クォータは切り捨てられ、最低でも 1 として数えられるため、--cpus=1.5 は 1 として数えられます。

2 種類の制限は数えられず、そのどちらかだけで抑えられているコンテナは、見えているすべての CPU を基準にサイズが決まります。

  • CPU の重み(docker run --cpu-shares、cgroup v2 の cpu.weight、v1 の cpu.shares)は、競合した CPU 時間を隣接するプロセスとどう分け合うかを決めるものです。上限ではなく、そこから CPU 数は導かれません。
  • プラットフォームはマシンの一部を販売し、その制限をコンテナから見えない場所で強制することがあります。たとえば DigitalOcean App Platform では、1 vCPU として販売されたインスタンスが、クォータなし(cpu.cfs_quota_us が -1)で 8 個の CPU が見える gVisor サンドボックス内で動作しているのが観測されました。そのためデフォルト値は、PHP ワーカー 4、ランタイムスレッド 4、キュー 512 になりました。
見えないところで制限を強制するプラン

そのようなプランでは、PHP_WORKERS と TOKIO_WORKERS を、プランが実際に提供するものに合わせて明示的に設定してください。ワーカーモードでは、PHP_WORKERS × WORKER_MAX_MEMORY_MIB もプランのメモリと照らし合わせて確認してください。このしきい値はワーカーがループ内でチェックするリサイクルのトリガーであって、PHP ヒープが割り当てられる量の上限ではありません。そのため、それを理由にワーカーがリサイクルされる前に、プール全体でその量、あるいはそれ以上を保持することがあり、その上に OPcache とサーバー自体の分が加わります。デフォルトの 0 では、しきい値そのものがありません。同じサンドボックスはメモリ制限も隠すことがあります。上記の App Platform のコンテナには、メモリ制限がまったく見えていませんでした。

起動時に、サーバーはこの数、それを決めたもの、そして結果として得られる PHP_WORKERS と TOKIO_WORKERS をログに記録します。以下は、14 CPU のマシンで docker run --cpus=1 の下で実行したときのものです。

json
{"timestamp":"2026-10-05T18:55:39.801254Z","level":"INFO","fields":{"message":"CPU count for default sizing","cpus":1,"cpu_source":"cgroup quota","host_cpus":14,"php_workers":"1 (auto)","tokio_workers":"1 (auto)"}}
  • cpu_source は、マスクもクォータも数を引き下げなかった場合は host、そのいずれかが引き下げた場合は affinity または cgroup quota、マスクまたはオンラインの CPU 数を読み取れずにサーバーが判断できなかった場合は unknown になります。CPU をまったく数えられなかった場合、cpus は 4 になります。
  • host_cpus は、システムがオンラインと報告している CPU の数(Linux では /sys/devices/system/cpu/online)で、マスクやクォータでは引き下げられません。読み取れない場合は省略されます。このファイルをコンテナの CPU だけを列挙するように書き換えるランタイムでは、絞り込まれたマスクが host として読まれます。
  • (auto) は、完全にデフォルトに任された値を示します。変数が未設定または空である場合か、TOKIO_WORKERS については数値でない場合です。

cpus より少ない CPU を販売したプランで cpu_source が host になっているなら、それが上記のケースです。

ワーカーモード

変数 デフォルト 説明
WORKER_MAX_MEMORY_MIB 0 ワーカーがリサイクルされるまでの、ワーカーごとの最大メモリ(MiB)。0 = 無制限

WORKER_MODE_ENABLED=true を設定し、ENTRY_FILE をワーカーのブートストラップスクリプト(例: ENTRY_FILE=worker.php や ENTRY_FILE=../worker.php)に向けます。すると PHP ワーカーはリクエストをまたいで生き続け、ブートストラップの状態(オートローダー、データベース接続)をメモリ上に保持します。ワーカーは WORKER_MAX_MEMORY_MIB を超えたとき、または破綻したリクエストを 3 回連続で受け持ったときに自動的にリサイクルされ、アプリケーションが Worker::scheduleExit() を呼び出したときにはオンデマンドでリサイクルされます。3 番目の条件に何が数えられるかはリサイクルを参照してください。以前のリリースにあった WORKER_MAX_REQUESTS のつまみは非推奨となり無視されます。どちらも設定しないか、Worker::scheduleExit() へ移行してください。

非推奨: INDEX_FILE と WORKER_FILE

レガシーの INDEX_FILE と WORKER_FILE 変数は、後方互換性のために今も解析されます。設定されると起動時に WARN ログ行を出力し、新しいモデルへマッピングされます。

レガシー 現在の同等設定
INDEX_FILE=index.php ENTRY_FILE=index.php
INDEX_FILE=index.html ENTRY_FILE=index.html
WORKER_FILE=/path/worker.php WORKER_MODE_ENABLED=true ENTRY_FILE=/path/worker.php

古い設定と新しい設定の両方が指定された場合は、ENTRY_FILE / WORKER_MODE_ENABLED が優先されます。移行はご都合のよいタイミングで行ってください。非推奨の形式は将来のリリースで削除される予定です。

SAPI / PHP

変数 デフォルト 説明
SUPERGLOBALS_ENABLED true スクリプトの実行前にリクエストを PHP に記述し、PHP が $_SERVER と $_GET を構築できるようにします。どちらも読まないアプリケーションでは、偽値を設定するとその処理をスキップできます。影響するのはこの 2 つだけです。$_POST、$_FILES、$_COOKIE はリクエストボディと Cookie ヘッダーから構築され、それらはいずれにせよ PHP に渡されます。また php://input はスーパーグローバルではなくストリームです。完全な表は SUPERGLOBALS_ENABLED を参照してください

タイムアウト

変数 デフォルト 説明
HEADER_TIMEOUT_SECONDS 5 接続後に HTTP ヘッダーを受信するまでの最大秒数(Slowloris 対策)
DRAIN_TIMEOUT_SECONDS 25 グレースフルシャットダウン中に処理中の接続を待つ最大秒数。期限の時点でブロッキング呼び出しの中にあるリクエストは、プロセスをそれより長く引き留めることがあります(グレースフルシャットダウンを参照)

PHP の実行時間は、OxPHP の環境変数ではなく、PHP 自身の max_execution_time ini ディレクティブ(および実行時の set_time_limit())によって制限されます。この制限は非同期タスクには及びません。ASYNC_WORKERS プールのスレッドは実行デッドラインを持たないため、ディレクティブも set_time_limit() も、oxphp_async() でディスパッチされたタスクが実行する処理を制限しません。そのようなタスクは、代わりに oxphp_async_await() のタイムアウト引数で制限してください。非同期 Promiseを参照してください。

レート制限

変数 デフォルト 説明
RATE_LIMIT 0(オフ) 時間ウィンドウあたり、IP ごとの最大リクエスト数。0 はレート制限を無効化します
RATE_WINDOW_SECONDS 60 レート制限のウィンドウ長(秒)

セキュリティ

変数 デフォルト 説明
FRAME_OPTIONS SAMEORIGIN クリックジャッキング対策。SAMEORIGIN は同一オリジンのページによるフレーム化のみを許可、DENY はすべてのフレーム化をブロック、off は無効化(フレーム化を独自の CSP で管理する場合に使用)。それ以外の値は起動時の警告とともにデフォルトの SAMEORIGIN にフォールバックします。すべてのレスポンスに X-Frame-Options と Content-Security-Policy: frame-ancestors の両方を設定します。発行されるヘッダー値、サーバーヘッダーがアプリケーション設定のヘッダーにどう譲るか、値の選び方の指針については、下のクリックジャッキング対策を参照してください
TRUSTED_PROXIES (未設定) 信頼済みリバースプロキシのネットワーク。CIDR、単独の IP、キーワード private と cloudflare をカンマ区切りで組み合わせて指定します(例: private,cloudflare)。設定すると、OxPHP は Forwarded(RFC 7239)または X-Forwarded-For ヘッダーから、最も右側の非信頼アドレスを採用するアルゴリズムで実際のクライアント IP を抽出します。cloudflare を指定した場合、転送チェーンから Cloudflare のエッジから到着したことがわかるリクエストについては、代わりに CF-Connecting-IP から取得します。また X-Forwarded-Proto と X-Forwarded-Host を処理して $_SERVER['HTTPS']、REQUEST_SCHEME、SERVER_NAME、SERVER_PORT に反映します。未設定 = 機能は無効
PHP_DENY_PATHS (未設定) .php ファイルを直接 URI 経由で決して実行してはならない、カンマ区切りの glob パターン(例: /uploads/**,/cache/**,/admin/legacy.php)。パターンはディレクトリ全体または単一のファイルを対象にできます。直接マッピングのモード(Traditional と SPA)で適用されます。Framework と Worker モードでは、任意の .php ファイルを直接実行することがないため、起動時に警告を出して無視されます。ディレクトリインデックスの解決(/uploads/ → uploads/index.php)を通じて到達するスクリプトもカバーします。直接の .php URI については、マッチングがディスク I/O の前に行われるため、拒否されたパスはファイルの有無にかかわらず同じレスポンスを返します(存在を推測できるオラクルになりません)。レガシー名の PHP_DENY_DIRS は非推奨のエイリアスとして受け付けられ、起動時に WARN を出力します。PHP 実行拒否リストを参照
PHP_DENY_FALLBACK 404 PHP_DENY_PATHS にマッチしたとき、または .oxphpdeny の拒否ルールに該当したときに返す内容。400–599 の HTTP ステータス(ERROR_PAGES_DIR と組み合わせてカスタム HTML を返す)、または DOCUMENT_ROOT 内の PHP フォールバックスクリプトを指す / で始まる URI パスのいずれか。フォールバックスクリプトは $_SERVER で OXPHP_DENIED_PATH と OXPHP_DENIED_PATTERN を受け取ります。起動時に検証されます。スクリプトは存在し、DOCUMENT_ROOT 内に正規化され、かつそれ自身が PHP_DENY_PATHS にマッチしてはなりません(ループ防止)。.oxphpdeny のルールがスクリプトをカバーすることは許されます。ワーカーモードでは、.oxphpdeny の拒否ルールはステータスコードしか取りません。パス拒否ルールを参照
SYMLINK_ALLOW_PATHS (未設定) シンボリックリンクが DOCUMENT_ROOT の外へ抜け出すことを許可する、カンマ区切りの絶対パスのリスト。各項目はディスク上にすでに存在している必要があります。相対パスや存在しないパスは起動を中止させます。未設定 = シンボリックリンクの抜け出しは一切許可しません。シンボリックリンク許可パスを参照

特殊値 private は、すべての RFC-1918 プライベートネットワーク、ループバック、およびリンクローカルアドレス(IPv4 と IPv6)に展開されます: 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16、::1/128、fc00::/7、fe80::/10。

クリックジャッキング対策

クリックジャッキングは、敵対的なページがあなたのサイトを不可視の <iframe> に埋め込み、ユーザーに見えないものをクリックさせる攻撃です。「はい、アカウントを削除します」ボタン、ワンクリック購入、OAuth の「認可」プロンプトなどです。防御は、誰が(もし誰かいるなら)あなたのページをフレーム化してよいかをブラウザに伝えることです。これを制御するのが FRAME_OPTIONS です。

フレーム化を支配するヘッダーは 2 つあります — レガシーの X-Frame-Options(すべてのブラウザが理解します)と、モダンな Content-Security-Policy: frame-ancestors(両方が存在する場合はこちらが優先されます)です — ので、OxPHP は両方を発行し、ポリシーは古いブラウザでも新しいブラウザでも等しく保たれます。単一の FRAME_OPTIONS 値は、対応するペアにマッピングされます。

FRAME_OPTIONS X-Frame-Options Content-Security-Policy 誰がページをフレーム化できるか
SAMEORIGIN(デフォルト) SAMEORIGIN frame-ancestors 'self' 同一オリジンのページのみ
DENY DENY frame-ancestors 'none' 誰も不可。自分自身のページでさえ不可
off (送信されない) (送信されない) 誰でも — サーバーからのフレーム化制限なし

値の選び方。 デフォルトは SAMEORIGIN です。クリックジャッキングが実際に依存しているクロスオリジンのフレーム化をブロックしつつ、自分のページ同士の埋め込み — 多くのアプリが正当に行っていること(管理画面のプレビュー、ダッシュボードのウィジェット、同一オリジンでホストされる決済コンポーネント)— は引き続き許可します。サイト上の何ひとつ、自分自身によってさえフレーム化されることを意図していない場合は、最も厳格な姿勢として DENY を選んでください。off を選ぶのは、アプリケーションが設定する完全な Content-Security-Policy によって自分でフレーム化を管理する場合だけです — 後述を参照。

外部オリジンによるフレーム化。 どちらの X-Frame-Options 値も、特定の許可オリジンを名指しすることはできません(ALLOW-FROM は標準から削除されました)。指名したサードパーティにページのフレーム化を許可するには、FRAME_OPTIONS=off を設定し、アプリケーションに明示的な frame-ancestors リストを持つ独自の Content-Security-Policy を発行させてください。例: header("Content-Security-Policy: frame-ancestors 'self' https://partner.example.com");

アプリケーションのヘッダーが優先されます。 サーバーのヘッダーはフォールバックであり、レスポンスがそのようなヘッダーを持たない場合にのみ適用されます。PHP の header() で独自の X-Frame-Options や Content-Security-Policy を設定するアプリケーションは、それをそのまま保持します。フレーム化に関する 2 つのヘッダーは 1 つのポリシーとして扱われるため、サーバーがアプリケーションと矛盾することは決してありません。

  • アプリが X-Frame-Options を設定している場合、OxPHP は自身の frame-ancestors フォールバックをスキップします(サーバーの CSP はモダンブラウザでアプリの選択を上書きしてしまうためです)。
  • アプリが frame-ancestors ディレクティブを含む Content-Security-Policy を設定している場合、OxPHP は自身の X-Frame-Options フォールバックをスキップします(より厳しいサーバーの X-Frame-Options は、CSP を無視するレガシーブラウザで過剰にブロックしてしまうためです)。

同じ優先順位は、OxPHP がすべてのレスポンスに nosniff として設定する X-Content-Type-Options にも適用されます。アプリケーションが設定した値はそのまま保持されます。何かの効果を持つ値は nosniff だけであることに注意してください — アプリケーションがそれ以外の値で上書きすると、MIME スニッフィング保護は静かに無効になります。

TLS

変数 デフォルト 説明
TLS_CERT (未設定) PEM 形式でエンコードされた TLS 証明書のパス。TLS を有効化するには TLS_CERT と TLS_KEY の両方を設定する必要があります
TLS_KEY (未設定) PEM 形式でエンコードされた TLS 秘密鍵のパス
TLS_MIN_VERSION 1.2 受け付ける最低の TLS プロトコルバージョン: 1.2 または 1.3。TLS が有効でない場合でも、起動時に(および oxphp config --check によって)検証されます — 非 UTF-8 バイトを含め、それ以外の値は致命的な起動エラーになります。空の値は未設定として扱われます

HTTP/2

変数 デフォルト 説明
H2_MAX_CONCURRENT_STREAMS PHP_WORKERS_MAX × 4(最小 32) HTTP/2 接続あたりで同時に開けるストリームの最大数
H2_MAX_PENDING_RESET 20 接続が閉じられるまでにキューへ積まれる RST_STREAM フレームの最大数(Rapid Reset 対策)
H2_MAX_HEADER_LIST_BYTES 65536 リクエストあたりのデコード後ヘッダーの最大合計バイト数
H2_KEEPALIVE_INTERVAL_SECS 20 HTTP/2 PING フレームの間隔(秒)。0 で無効化
H2_KEEPALIVE_TIMEOUT_SECS 10 接続を閉じる前に PING の応答を待つ秒数

静的ファイル

変数 デフォルト 説明
STATIC_MAX_AGE 30d 静的ファイルの Cache-Control: max-age。30s、5m、2h、30d、1w、1y、秒数のみ(3600)、またはヘッダーを無効化する off を受け付けます。非推奨の STATIC_CACHE_TTL を置き換えます。
STATIC_REVALIDATE off ブール値 — ブール値を参照。真値を設定すると、メモリ内のコンテンツキャッシュで mtime による再検証を有効化します。ファイルの更新時刻はファイルごとに(リクエストごとではなく)最大でも 3 秒に 1 回だけ再チェックされ、古いエントリは自動的に破棄されるため、変更は 3 秒以内に反映されます。非推奨の STATIC_CACHE(off が逆の意味だった)を置き換えます。

圧縮

変数 デフォルト 説明
COMPRESSION_ENCODINGS br,zstd,gzip サーバーが提供するコンテンツコーディング。カンマ区切りで br(または brotli)、zstd、gzip。未知の名前は起動エラーになります。値全体を off(または none)にすると圧縮を完全に無効化します。これはリストのトークンではないため、br,off はスイッチではなくエラーになります。空の代入はデフォルトに戻します。ここに書いた順序は無視されます。キャッシュされた静的ファイルでは、コストを一度だけ払えばよい Brotli を優先し、それ以外はすべて Zstandard を優先します。圧縮を参照
COMPRESSION_BROTLI_LEVEL 5 Brotli の品質(0 から 11)。非推奨の COMPRESSION_LEVEL を置き換えます
COMPRESSION_ZSTD_LEVEL 6 Zstandard の圧縮レベル(0 から 19)
COMPRESSION_GZIP_LEVEL 6 Gzip の圧縮レベル(0 から 9)
COMPRESSION_LEVEL (未設定) COMPRESSION_BROTLI_LEVEL の非推奨名。起動時に WARN を出力します。COMPRESSION_LEVEL=0 は従来の意味を保ち、Brotli だけでなくすべてのコーディングを無効化します。明示的な COMPRESSION_BROTLI_LEVEL は 0 以外の値には優先しますが、0 には優先しません。0 は他に何が設定されていても圧縮を無効化します

コーディングが提供されるのは、COMPRESSION_ENCODINGS がそれを列挙し、かつそのレベルが 0 でない場合です。どちらか一方だけでもそのコーディングは取り下げられます。範囲外のレベルや数値でないレベルは、変数名を示すエラーで起動を中止させます。値がクランプされることはありません。

ロギング

変数 デフォルト 説明
LOG_LEVEL info ログの詳細度: trace、debug、info、warn、error
ACCESS_LOG (未設定) リクエストごとのアクセスログ: all = すべてのリクエスト、error = 4xx/5xx のみ、未設定 = オフ
Note

ACCESS_LOG は all または error を受け付けます。アクセスログを完全に無効化するには未設定のままにします。

サーバーが応答する前に接続を閉じたクライアントは、error ではなく debug で記録されます。これは公開リスナーが目にする最もありふれた出来事(キャンセルされた fetch、閉じられたタブ、読み取りタイムアウトが発火したプロキシ)であり、サーバーの障害ではないため、デフォルトの LOG_LEVEL では見えません。件数は oxphp_request_cancelled_total{reason="client_abort"} で数えるか、LOG_LEVEL=debug に上げて個々の接続を確認してください。サーバー自身が強制するタイムアウト(HEADER_TIMEOUT_SECONDS)は warn のままで、それ以外のすべての接続失敗は error のままです。

LOG_LEVEL はアクセスログをフィルタリングしない

この 2 つは独立しています。LOG_LEVEL はサーバー自身の診断の詳細度を設定するものなので、LOG_LEVEL=warn と ACCESS_LOG=error の組み合わせは、警告とエラーを報告すると同時に、4xx/5xx のすべてのリクエストを記録します。アクセスログだけを黙らせるには、そのターゲットを名指しする RUST_LOG ディレクティブが必要です。RUST_LOG=warn,access_log=off の warn は、それまで使っていた LOG_LEVEL に置き換えてください。RUST_LOG は、設定されていて解析可能な場合、LOG_LEVEL に重ねられるのではなく、それを置き換えます。アクセスログを参照してください。

可観測性

変数 デフォルト 説明
INTERNAL_ADDR (未設定) 内部サーバー(/health、/metrics、/config)のアドレス。未設定のときは内部サーバーを起動しません。ポートのみの値(:9090)は 127.0.0.1 にバインドします。ホスト外へ公開するには、明示的に 0.0.0.0:9090 をバインドしてください
INTERNAL_ALLOW_IPS (未設定) 内部サーバー向けの、カンマ区切りの CIDR/IP 許可リスト。リスト外のピアは /metrics、/config、およびプラグインパスで 403 を受け取ります。ヘルスプローブ(/health、/healthz、/readyz、/startupz とそれらの長い形式)は引き続きアクセス可能です。未設定/空 = すべて許可。ループバックは暗黙には含まれません — localhost からのアクセスを維持するには 127.0.0.1/32 を列挙してください。不正な形式のリストは起動を中止させます
ERROR_PAGES_DIR (未設定) {status}.html という名前のカスタムエラーページ(例: 404.html、503.html)を含むディレクトリ
MAX_QUERY_BODY 524288 内部クエリエンドポイント向けのリクエストボディの最大サイズ(バイト)(512 KiB)
TRACE_CONTEXT false ブール値 — ブール値を参照。真値のとき、W3C Trace Context の伝播を有効化します。traceparent/tracestate ヘッダーを読み取り、$_SERVER を介して PHP に転送します

OpenTelemetry

変数 デフォルト 説明
OTEL_ENABLED false OpenTelemetry のスパンエクスポートを有効化します。自動的に TRACE_CONTEXT=true を設定します。ブール値 — ブール値を参照
OTEL_EXPORTER_OTLP_PROTOCOL grpc エクスポートプロトコル: grpc または http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317(gRPC)または http://localhost:4318(HTTP) OTLP コレクターのエンドポイント。http/protobuf の場合はベース URL で、スパンはその下の /v1/traces に送信されます
OTEL_EXPORTER_OTLP_TIMEOUT 10000 エクスポートのタイムアウト(ミリ秒)。gRPC では、TLS ハンドシェイクを含むコレクターへの接続も制限するため、それにかかる時間より大きく保ってください。先に接続する必要がある試行は、タイムアウトの最大 2 倍の時間がかかることがあります。Linux 上の gRPC では、データを受け付けなくなった接続がどれだけ早く閉じられるかも決めます — ログに出るスパンエクスポートのエラーを参照
OTEL_EXPORTER_OTLP_HEADERS (未設定) 認証ヘッダー: key=value,key2=value2
OTEL_SERVICE_NAME oxphp エクスポートされるスパンでのサービス名
OTEL_SERVICE_VERSION (未設定) サービスバージョンの属性
OTEL_RESOURCE_ATTRIBUTES (未設定) 追加のリソース属性: env=prod,region=us-east-1
OTEL_TRACES_SAMPLER parentbased_traceidratio サンプリング戦略: always_on、always_off、traceidratio、parentbased_always_on、parentbased_always_off、parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG 1.0 比率ベースのサンプラー向けのサンプリング比率(0.0–1.0)
Note

範囲外または不正な OTEL_TRACES_SAMPLER_ARG の値は [0.0, 1.0] にクランプされ、warn レベルでログに記録されます。未知の OTEL_TRACES_SAMPLER の値は parentbased_traceidratio にフォールバックし、ログに記録されます。

APM

変数 デフォルト 説明
OTEL_APM_ENABLED false APM を有効化します: 自動インストルメンテーション、エラーキャプチャ、および PHP トレーシング SDK。OTEL_ENABLED=true が必要です。ブール値 — ブール値を参照
OTEL_APM_SLOW_QUERY_MS 100 スロークエリのしきい値(ミリ秒)。これを超えるデータベースクエリには oxphp.db.slow=true スパン属性が付きます
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED false バインドパラメータを db.params スパン属性に記録します。パラメータに機密データが含まれる可能性がある場合は、本番環境で無効にしてください。ブール値 — ブール値を参照
OTEL_APM_STACKTRACE_MAX_BYTES 8192 exception.stacktrace 属性の最大サイズ(バイト)。上限を超えると、スタックトレースは末尾から切り詰められ …(truncated) マーカーが付きます。0 で切り詰めを無効化
OTEL_APM_MESSAGE_MAX_BYTES 4096 exception.message 属性の最大サイズ(バイト)(デフォルトは New Relic の属性ごとの値の上限に合わせています)。上限を超えると、メッセージは末尾から切り詰められ …(truncated) マーカーが付きます。0 で切り詰めを無効化

APM が有効なとき、OxPHP は 34 個の内部 PHP 関数(PDO、mysqli、cURL、Redis、Memcached、ファイル I/O)を自動的にフックして子スパンを作成します。oxphp_apm_*() PHP 関数は、APM が有効かどうかにかかわらず登録されます。無効なときは、スパン関数はセンチネル値を返す no-op になります。oxphp_apm_trace() は例外で、コールバックを実行してその値を返し続けるため、APM を無効にしてもアプリケーションが計算する内容が変わることはありません。

非同期ワーカー

変数 デフォルト 説明
ASYNC_WORKERS 0(無効) 専用の非同期ワーカースレッドの数。0 のとき、非同期関数(oxphp_async など)は登録されますが、呼び出すと OxPHP\Async\AsyncException をスローします。バックグラウンドタスクの実行を有効にするには正の値を設定します
ASYNC_QUEUE_CAPACITY ASYNC_WORKERS × 64 非同期キューで保留できるタスクの最大数。0 = 自動(ワーカー数 × 64)
ASYNC_MAX_FIBERS 256 ワーカーごとの、同時実行する非同期タスクファイバーの上限。プロセス全体での処理中(キュー待ち + 実行中)の上限は ASYNC_MAX_FIBERS × ASYNC_WORKERS です。これを超えるディスパッチは即座に OxPHP\Async\AsyncException で拒否されるため、ファンアウトの合成がデッドロックすることはありません

非同期ワーカープールは、PHP からディスパッチされた撃ちっぱなし(fire-and-forget)のバックグラウンドタスクを処理します。これは PHP ワーカープールとは別物で、標準的なリクエスト処理には必要ありません。

これら 3 つの変数のいずれかに不正な形式の値(例: ASYNC_WORKERS=8x)があると起動エラーになります — デフォルトにフォールバックすると、プールが静かに無効化されたり誤設定されたりするおそれがあるためです。ちょうど空の値は未設定として扱われます。

ランタイムフック

変数 デフォルト 説明
RUNTIME_HOOKS (無効) ブロッキングする PHP 組み込み関数を、ファイバーを中断する実装に置き換えるオプトイン機能。1/true/all はすべてのフックカテゴリを有効化します。カンマ区切りのリストは特定のカテゴリを有効化します(例: RUNTIME_HOOKS=sleep,streams)
カテゴリ フックする対象
sleep ネイティブの sleep() と usleep() が、oxphp_sleep()/oxphp_usleep() とまったく同じように現在のファイバーを中断します。この 2 つも中断できずワーカースレッド上で待つコンテキストでも、同じ振る舞いになります(sleep フックが中断しない場所を参照)
streams 2 つの待機が、ワーカースレッドを釘付けにする代わりに現在のファイバーを中断します。tcp:// ソケットストリーム上のブロッキング読み取りと、stream_select() です。読み取りは 1 つのソケットでブロックするクライアント — fsockopen()、stream_socket_client()、HTTP ストリームラッパー、mysqlnd(PDO_MySQL、mysqli)、phpredis — をカバーし、stream_select() は複数を同時に待つループをカバーします。どちらの場合もコード変更は不要です。それ以外の方法で待つクライアントは影響を受けません(後述)
net PHP ストリームより下層にあり、streams の読み取りフックからは見えない待機が、ワーカースレッドを釘付けにする代わりに現在のファイバーを中断します。stream_socket_client()、fsockopen()、および PHP ストリーム上に構築されたすべてのクライアント(mysqlnd、つまり PDO_MySQL と mysqli、そして phpredis など)による接続、TLS のハンドシェイク、暗号が有効になった後の ssl:///tls:// ストリーム上の読み取り、送信バッファの空きを待つ必要がある書き込み、そして unix://、udp://、udg:// ソケット、stream_socket_pair() のストリーム、TCP 以外のソケットから socket_export_stream() が作るストリーム上のストリーム読み取りです。streams を一緒に列挙する必要があり、Linux でのみ動作します(net が無効のままになる場合を参照)。対象外: libcurl、libpq(pgsql、pdo_pgsql)、libmemcached の内部での待機(これらは自身のライブラリから poll() を呼び出します)、DNS 解決、stream_socket_recvfrom()、タイムアウトが無限(default_socket_timeout=-1)のプレーンな(TLS でない)ストリーム上の書き込み、そして永続的な接続(pfsockopen()、STREAM_CLIENT_PERSISTENT、およびそれらの上に構築されたクライアントの永続モード)

フックは、ワーカーモードのリクエストファイバーと非同期タスクファイバーの内側で効果を発揮します。ファイバーの外側(traditional/framework/SPA のリクエストコンテキスト、CLI)では、引数バリデーションエラーを含め、元のネイティブな挙動が保たれます。sleep フックを有効にすると、sleep() を呼び出すサードパーティのコードは、ファイバーを中断できる場所(すべての場所ではありません。後述)ではワーカースレッドを釘付けにしなくなります。どちらの場合もコード変更は不要です。フックされたスリープの最中に非同期タスクをキャンセルすると OxPHP\Async\AsyncException で巻き戻され、中断したフックされた sleep() は 0 を返します。ネイティブ組み込みのシグナル割り込み時の戻り値が発生するのは、下のブロッキングパスの場合だけです。

設定が反映されたことを確認する

サーバーは、インストールしたカテゴリを解析済みの集合として 2 か所で報告します。内部サーバーでは次のとおりです。

Installed hook categories
$ curl -s localhost:9090/config | jq .runtime_hooks [ "sleep", "streams", "net" ]

内部サーバーに到達できない、または有効になっていないデプロイメントでは、PHP から確認できます。

php
<?php echo json_encode(oxphp_server_info()['runtime_hooks']); // ["sleep","streams","net"]

どちらも同じ値を読み、文字列ではなく集合を報告します。RUNTIME_HOOKS=all、RUNTIME_HOOKS=1、RUNTIME_HOOKS=sleep, streams, net はいずれも ["sleep","streams","net"] として返ってきます。空のリストは、フックが 1 つもインストールされていないことを意味します。変数が未設定の場合、RUNTIME_HOOKS=0 の場合、このビルドが知らないカテゴリだけを名指しした値の場合、そして変数名を綴り間違えた場合の答えはいずれもこれです。両者が混ざった値は事情が異なります。RUNTIME_HOOKS=sleep,bogus は ["sleep"] と読まれるため、空でないリストだけでは、すべての名前が理解されたことの証拠になりません。認識されなかった名前は、起動時にサーバーログにそれぞれ 1 行ずつ出力され、それが名前が示される唯一の場所です。設定を変更したら必ず確認してください。認識されない環境変数はどこでもエラーにならないため、「綴り間違い」はそれ以外ではレイテンシとしてしか表面化しません。

インストール済みは有効と同じではない

このリストが報告するのは、どの組み込み関数のハンドラーが置き換えられたかであり、それはルーティングモードにかかわらず起動時に行われます。ファイバーがまったく存在しない場所では、置き換えられたハンドラーは元のハンドラーに委譲するため、traditional モードのサーバーは ["sleep","streams","net"] を報告しながら、終始ネイティブに振る舞います。php CLI プロセスは ["sleep","streams"] を報告します。エンジンがその実行ファイルの一部であるため、net は PHP の待機とプログラム自身の待機を区別できず、無効のままになるからです。リストは「このプロセスはフックの準備ができている」と読み、それを使えるものがあるかどうかはルーティングモードで判断してください。

streams の中ではさらに範囲が狭くなります。ストリームの ops テーブルを書き込み可能にできないプラットフォームでは、ソケット読み取りの部分だけが単独でインストールに失敗することがあり、その場合も stream_select() とデータベースのエントリポイントはフックされたままです。カテゴリはどちらの場合も報告されます。唯一のシグナルは、ソケット読み取りがブロッキングのままであることを伝えるサーバーログの 1 行です。その行は、認識されないカテゴリの行や net が書き込む行(後述)と同様に、PHP のエラーログ設定に従います。error_log が未設定(デフォルト)ならサーバー自身のログに warn として届き、LOG_LEVEL=error ではそれが隠れます。

net が無効のままになる場合

net は、実際に有効になっている場合にのみ報告されます。streams なしで列挙された場合は無効のままで、その旨を伝える行がサーバーログに出力されます。Linux 以外のプラットフォームの場合、起動時にサーバー自身のコードと PHP のコードを区別できない場合、あるいは PHP の poll() がサーバーの横取りするものにバインドされていない場合は、接続と TLS の待機がブロッキングのままであることを伝える行とともに無効のままになります。したがって、要求したのにリストに net がないことは、それ自体が答えであり、報告の抜けではありません。

リストが教えてくれない唯一のことは、OpenSSL がどこにあるかです。OpenSSL が行う待機(ssl:// の接続の最後に行われるものを含む TLS ハンドシェイク、および暗号が有効になった後の TLS ストリーム上の読み取りと書き込み)がファイバーを中断して待機するのは、公式イメージのように OpenSSL が PHP に組み込まれている場合だけです。OpenSSL が別個の共有拡張として読み込まれている場合(extension=openssl)、サーバーから見るとそれらの待機は PHP 自身のものではないため、スレッドを保持し続けます。一方、接続、unix://、その他の読み取りは引き続き中断し、net も引き続き報告されます。

sleep フックが中断しない場所

ファイバーの切り替えが拒否される場所では、フックされた sleep()/usleep() は置き換えた組み込み関数に呼び出しを渡します。組み込み関数はワーカースレッド上で待ち、自身の戻り値を返します。したがって、そこでシグナルによって中断された sleep() は、ファイバーの外側と同じように、眠らなかった残りの秒数を返します。(引数チェックはどちらの場合もフック自身のものです。4294967295 を超える値を拒否しますが、フックされていない組み込み関数はそれを受け付けて切り詰めます。)現在のフレームを離れることが安全でない場所ではどこでも切り替えがブロックされます。以下のリストは網羅的ではありません。

  • declare(ticks) ハンドラーの内側と、pcntl のシグナルディスパッチの内側(どちらも上流 PHP 自身によるもの)。
  • リクエストの入力を構築している間。
  • フィールドごとの定義配列を渡された filter_input_array() から到達する FILTER_CALLBACK の内側(スーパーグローバルの $_GET の注記を参照)。
  • リクエストが破綻した後(致命的エラーの後、または既にキャンセルされたリクエストへの書き込みの後)にデストラクタを実行する破棄処理の内側。したがって、バックオフのためにスリープする __destruct() は、スレッドを保持したままそれを行います。

リクエストの通常の終了はこれらに含まれません。そこではデストラクタとシャットダウン関数は切り替えが許された状態で実行されます。oxphp_sleep() と oxphp_usleep() もこれらすべての場所でブロッキングパスを取るため、それらに頼ってもこれを回避することはできません。

ユーザーランドのスケジューラ(AMPHP、Revolt、あるいは素の new Fiber)が開始したファイバーは別のケースです。そこでは切り替えはブロックされず、スケジューラは自分のファイバーを通常どおり中断・再開しますが、OxPHP は自分が所有していないコンテキストを再開できないため、streams フックと同じ理由でフックは身を引きます。これはリクエストの内側で開始されたファイバーの話です。ワーカーモードのリクエスト自体は OxPHP が駆動するファイバーなので、フックはそれに通常どおり適用されます。アプリケーションが自分で開始したファイバーの内側のコードはワーカースレッド上でスリープするため、その分の負荷はフックがオフであるかのようにサイズを見積もる必要があります。

streams フックがカバーする範囲

streams フックが協調的にするのは、PHP ソケットストリーム上のブロッキング読み取りと、stream_select() 内の待機です。これを当てにする前に、あなたのクライアントがこの 2 つのいずれか、または net カテゴリが追加する方法(後述)のいずれかで待っていることを確認してください。よくあるクライアントのいくつかはそうではなく、それらにとっては何も変わりません。

  • ext/curl。 curl_exec() と curl_multi_* は PHP ストリームの下でソケットと直接会話するため、フックの目に触れることはありません。これは curl 上に構築されたすべての HTTP クライアントに当てはまります — デフォルトハンドラーの Guzzle もその 1 つです。curl クライアントは代わりにストリームラッパーハンドラーへ向けることができ、そちらは PHP ストリームを経由します。

  • socket_select()。 これは ext/sockets という、生のディスクリプタ上の別の API であり、フックされません。stream_select() はフックされます。同じことは stream_socket_accept() 内の待機にも当てはまり、こちらもフックされません。

  • unix://、udp://、udg:// ストリーム、ソケットペア、TCP 以外のソケットから socket_export_stream() で得たストリーム、そして暗号が有効になった後の ssl:///tls://。 これらの ops テーブルは PHP または OpenSSL に閉じているため、streams フックは届きません。net カテゴリは届きます。net を列挙すると、これらのいずれかの上でのストリーム読み取り(fread()、fgets() など)は、tcp:// のものと同じようにファイバーを中断します。net がなければ、これらは従来どおりスレッドを保持します(stream_socket_enable_crypto() が成功する前は、SSL ストリームの読み取りはプレーンなソケット読み取りに委譲されるため、どちらの場合も中断します)。stream_socket_recvfrom() はストリーム読み取りではありません。これは recvfrom() を直接呼び出し、中断すべき PHP 自身の待機がないため、udp:// や udg:// ソケット上では、net があってもデータグラムが届くまでスレッドを保持します。socket_export_stream() は、ソケットが使用するトランスポートのストリームを作ります。UDP、unix、データグラム unix のソケットはそれぞれ udp://、unix://、udg:// ストリームになり、TCP ソケットは通常の tcp:// ストリーム(streams だけで既にカバーされます)になり、それ以外の種類のソケットは、ソケットペアが持つのと同じ汎用ストリームになります。

  • 接続の確立と DNS 解決。 net があれば、PHP のストリームトランスポートを通じた接続はファイバーを中断します。stream_socket_client()、fsockopen()、およびそれらの上に構築されたクライアントです。ftp_connect() と socket_connect() はストリーム層の外で独自に接続するため、カバーされません。DNS 解決も中断しません。ホスト名は接続が始まる前にワーカースレッド上で解決されるため、遅いリゾルバーはスレッドを保持します。独自のネットワークライブラリを持つクライアント(libpq 経由の pdo_pgsql と pgsql、libmemcached 経由の memcached 拡張)は、そのライブラリの内部で接続して待機するため、どちらのカテゴリからも見えません。接続を開いたまま保持するクライアントは、いずれにせよクエリのたびに恩恵を受けます。

    永続的な接続は例外であり、それは pfsockopen() と STREAM_CLIENT_PERSISTENT にとどまりません。PDO::ATTR_PERSISTENT を指定した PDO_MySQL、p: ホストプレフィックスを付けた mysqli、pconnect() を使う phpredis もストリームを永続的なものとして開き、それらの接続は、net が存在する前と同じように、待機の間スレッドを保持します。理由は永続リストにあります。永続ストリームは接続される前にそのキーでリストに登録されるため、最初のファイバーが待っている間に同じキーを求めた 2 つ目のファイバーは、まだディスクリプタを持たないストリームを見つけ、それを死んだ接続とみなし、接続中のファイバーの下でそれを閉じてしまいます。すべての接続に固有のキーを与えるクライアントもあり(mysqlnd はそうです)、そうしたストリームはその方法では誰にも見つけられませんが、サーバーは誰が見つけられるかではなくストリームが永続的かどうかで判断するため、それらもスレッドを保持します。コストは接続にかかるもので、永続接続はそれを最初に開いたときに一度だけ支払います。その上での読み取りと書き込みにはかかりません。

  • localhost 経由で到達する MySQL。 MySQL クライアントは localhost を「unix ソケットを使え」と読みますが、それは tcp:// ストリームではないため、streams フックからは見えません。代わりに DSN に 127.0.0.1 と書くか、unix:// ストリームをカバーする net を列挙してください。これは PDO_MySQL と mysqli の両方に当てはまり、見落としやすい点です。すべては動き続け、ただ恩恵がないだけだからです。

  • 書き込み。 streams だけの場合、待つのは読み取りだけです。読み取りの準備完了状態は、1 つのファイバーがディスクリプタを所有し他の何もそれを排出しない限り持続しますが、送信バッファの空きはピアによって与えられたり取り消されたりするため、書き込み可能で起こされたファイバーは、書き込みが実行される頃には窓が再び閉じているのに気づくことがあり、その後 PHP はフックの有無に関係なくタイムアウトいっぱいまでスレッドをブロックします。したがって、ソケットバッファを満たす書き込みは、フックなしのときとまったく同じように振る舞います。実際にはこのコストは小さいものです。時間がかかるのは応答を待つことであって、クエリをカーネルへ渡すことではないからです。(書き込みが監視されるのは 1 つのことのためだけです。その接続が現在どのファイバーの交換を運んでいるか — 下の共有接続に関する注記を参照してください。他のファイバーが使っていない接続 — 通常の場合はすべての接続がそうです — では、書き込みは手つかずのネイティブパスを実行します。)

    net を列挙すると、空きを待つ必要がある書き込みもファイバーを中断し、起床のたびにディスクリプタが再確認されるため、その間に閉じた窓のコストはスレッドではなく、もう一度の中断で済みます。タイムアウトが無限(default_socket_timeout=-1)のプレーンなストリームは例外です。PHP はその上でブロッキングの send() によって送信し、自身で待つことがないため、中断するものがなく、書き込みはフックなしのときと同じようにスレッドを保持します。タイムアウトが無限の TLS ストリームは異なります。OpenSSL 自身のループが制限なしに poll() で待ち、その待機は中断されます。

この範囲内では、フックはネイティブの契約を保ちます。ソケットタイムアウト(stream_set_timeout()、default_socket_timeout)は変わらず適用され、タイムアウトした読み取りは引き続き stream_get_meta_data() を通じて timed_out を報告し、ストリームの同一性には手が付けられないため、socket_import_stream() などは動き続けます。タイムアウトには 1 つの制限があります。期限はスケジューラのティックごとに 1 回しか調べられないため、発火は早くても次のティックです — 最良でワーカーモードで 100 マイクロ秒、非同期プールで 50 マイクロ秒、負荷がかかればそれ以上です。ティックはワーカーが実行しているものが続く限り続くからです。非同期プールは何も起きていないときにはそれより長く休止しますが、保持している期限を越えて休止することはありません。読み取りまたは書き込みの期限は、休止をちょうどその上に着地するよう短縮するため、長く待つことでタイムアウトが遅くなることはありません。default_socket_timeout が 60 秒であれば、これはほとんどのデプロイメントで観測できるものではありません。

stream_select() は、その 3 つの配列が名指しするディスクリプタを待ってから、タイムアウトをゼロに設定して呼び出しを PHP に渡すという形でフックされます。したがって、観測できるものはすべて依然として PHP が決めます。戻り値のカウント、準備完了のストリームだけに配列を書き換えること、警告、そして引数エラーです。配列の中に、フックが肩代わりしないものが含まれている場合はいつでも、待機はスキップされ、呼び出しはフックなしのときとまったく同じように実行されます。既にデータがバッファされている読み取りストリーム(これには stream_select() はディスクリプタを見ずにバッファから答えます)、ディスクリプタをまったく持たないストリーム、生きたストリームでない要素(たとえば配列に残された閉じられたストリーム — PHP はこれに待機ではなくエラーで答えます)、カーネルが準備完了を監視してくれないディスクリプタ(通常のファイルがよくあるケースで、尋ねた瞬間に準備完了として数えられます)、あるいは FD_SETSIZE 以上のディスクリプタ(これは PHP 自身の select() が門前払いします)です。最後のものは形式的な話ではなく現実の上限です。ビジーなワーカーは 1024 を超えるオープンディスクリプタを保持しえますし、そのうちの 1 つを名指しする stream_select() は、フックの有無にかかわらず同じように失敗します。

並行ファイバー間で共有される接続

並行ファイバー間で共有される 1 つの接続は、OxPHP がガードするクライアントにとって安全ですが、何も得られません。 これはエッジケースではなく、ワーカーモードアプリケーションの通常の形です。WordPress、Laravel、Symfony は、ワーカーの起動時にデータベースとキャッシュのクライアントを一度だけ開き、同じものをすべてのリクエストに渡します。データアクセス層を書き直さない限り、ファイバーごとに 1 接続を求めることはできません。

クライアントプロトコルは交換の連続です — コマンドを書き、答えを読む — が、接続上には 1 つの交換がどこで終わるかを示すものは何もありません。読み取りで待機しているファイバーは交換の真っ最中で待機しているのであり、そこに 2 つ目のファイバーのコマンドが着地するとプロトコルは壊れます。この 2 つのクライアントはそこで異なる形で失敗し、どちらもうまくは失敗しません。mysqlnd は接続状態を追跡しており、何も送信する前にコマンドを拒否します。一方 phpredis にはそのようなチェックがなく、2 つのファイバーは互いの応答を読み合います — あるリクエストのデータが、エラーを一切上げずに別のリクエストへ返されるのです。

そのためファイバーは、接続を使用する前にそれをクレームします。これが行われなければならない 2 つのレベルの両方でです。バイト列の順序を保つソケット ops と、PDO および mysqli のクライアントエントリポイントです。mysqlnd の拒否はあらゆる I/O の前に起こるため、ソケット側では間に合わずそれを防げないからです。同じ接続に到達した別のファイバーは、クライアントレベルではそれが手放されるのを待ちます。そこで保持されているのは接続の同一性だけだからです。ソケットレベルでは待たずに、ソケットタイムアウトが拒否されるのと同じやり方で拒否されます。他人のストリーム上の操作の内側で中断されたファイバーは、所有者が解放するかもしれないポインタを保持していることになるからです。phpredis もまったく同じ理由で、メソッドごとにクライアントレベルでガードされています。クライアントレベルがクレームするのは接続そのものであって、それを保持する PHP オブジェクトではありません。したがって、複数の PDO オブジェクトを通じて到達される永続接続は 1 つとして数えられ、PDO::connect() — PDO ではなくドライバー自身のサブクラスを返します — で開かれた接続も他と同じようにカバーされます。

したがって、このフックの利得は、ファイバーが自分自身のために開く接続に属するものだと読んでください。独自の HTTP やデータベース呼び出しを行う非同期タスク、独自のクライアントを開くリクエストです。共有接続が得るのは、待っている間のワーカースレッドの返却であり、他のリクエストはその接続上にない仕事を実行できます。共有接続自身の交換は、フックがオフのときとまったく同じように、順番に 1 つずつ実行されます。知っておく価値のある境界が 4 つあります。

  1. 一度クエリしたファイバーは、その接続をリクエストの終わりまで保持します。 リクエストの終わりが、交換の終わりを確実に過ぎた最初の瞬間だからです。他の何かで待機している間も保持し続けるため、クエリした後に、同じ接続を必要とする自分自身の仕事を待つリクエストは、自分自身を待っていることになります。両者は先へ進む代わりに、下の制限時間で解けます。

  2. 待機は常に制限されます。 max_execution_time と default_socket_timeout の小さい方によってです。どちらも設定しなければその制限は 30 秒です。サーバー SAPI はエンジンのデフォルト — 前者は 30、後者は 60 — を取るからです。default_socket_timeout の 60 になるのは max_execution_time が 0 の場合だけです。max_execution_time はリクエストが現在持っている値として読まれます。set_time_limit() は、リクエストが自分はどれだけ実行してよいかを表明する手段だからです。default_socket_timeout はプロセスが起動したときの値として読まれます。これはソケット操作のデフォルト期限であり、あるリクエストが自分の呼び出しのためにそれを狭めた — ライブラリがよくやることで、後に残していきがちなものです — としても、同じワーカー上で後続するリクエストのためのこの制限を短くしてはならないからです。制限を超えると、呼び出しはガードなしの挙動にフォールバックし、理由をサーバーログに記します。PDO と mysqli の場合はクライアントに渡され、交換の途中で発行されたコマンドに対するクライアント自身の拒否が、アプリケーションが既に処理しているエラーになります。一方 phpredis にはそのような拒否がなく、代わりに他人の応答を読んでしまうため、RedisException を送出して何も送信しません。

    PDO の呼び出しの 1 つは、ここでも、どのファイバーの外側でも実行されるコード(後述)にとっても例外です。別のファイバーが保持する永続接続上の PDO::rollBack() は、その上で開いているトランザクションを見つけると、何も送信せずに false を返します。そのトランザクションは保持者のものであり、そのステートメントの間に送られた ROLLBACK はそれを終わらせてしまうからです。それが報告するエラーは、エラーモードに従った形で報告される、接続が既に記録しているエラーであり、保持者のものである可能性があります。PDO 自身のオブジェクト破棄も保持者に限定されます。リクエストやタスクが永続接続を保持している間、その上のハンドルを落としたときに、そこで開いているトランザクションをロールバックし、ドライバーのリクエスト終了時のクリーンアップを実行するのは、保持者がそのハンドルを最後に使った(構築した、またはその後にそれを通じて OxPHP がガードする PDO メソッドのいずれかを呼び出した)場合だけです。これはハンドルがどこで破棄されるかにかかわらず適用され、別のリクエストで PHP のサイクルコレクタが実行される場合も含みます。それ以外のハンドルはどちらもスキップします。誰も保持していない接続では、従来どおりすべてのハンドルが両方を行います。

    ソケットレベルの競合には独自の制限がありません。決して待たないからです。操作はタイムアウトと同じやり方で即座に失敗するため、stream_get_meta_data() は timed_out を報告します。これは呼び出し 1 回分より高くつくことがあります。クライアントは拒否された操作を失敗したコマンドより深刻なものと解釈して構わず、mysqlnd は交換の両側でそう解釈します。何も送信しなかった書き込みは接続を失われたものとしてマークして閉じ、空のまま終わった読み取りはその場で接続を失われたものとしてマークし、以後その接続上のすべてのコマンドは、ワイヤーに触れることなくその状態から応答されます。失われるのは 1 回の呼び出しではなく、何かがハンドルを保持している限り、接続全体です。ガードされたクライアントのコマンドがそもそもこの拒否に到達しないようにしているのが、上のクライアントレベルのクレームです。生のソケット上の手書きプロトコルは、どちらの種類の拒否された操作も、閉じられた接続と同じように扱うべきです。したがって、互いが待つものを互いに保持し合う 2 つのファイバーは、永遠に待ち合う代わりにその制限で解けます。

    どのファイバーの外側で実行されるコードも、クレームを待つことは一切できず、適用されるのはソケットレベルだけです。RUNTIME_HOOKS=streams を使ったワーカーモードでは、それは主に、他のリクエストが待機している間に、ワーカーループがリクエストの合間にサイクルを回収するときに実行するデストラクタです。そのデストラクタが行う、待つことになるソケット操作は、保持者が応答を待ってその上で待機している接続上では、タイムアウトと同じやり方で即座に失敗し、何も送信しません。それ以外の瞬間には実行されます。クライアントレベルはそこでは適用されません。そのクレームは保持者のリクエストの終わりまで続くため、保持者がその瞬間に使っていない接続(ほとんどの時間がそうです)をそのようなコードに拒否してしまうからです。拒否の代償は、上と同じくクライアント次第です。phpredis は呼び出しを失敗させ、保持者は自分の応答を保持します。mysqlnd は自身の状態からコマンドを拒否するか、保持者の交換がその状態を準備完了のまま残していた場合は、書き込みを拒否されて接続を閉じ、保持者のリクエストはポイント 4 で説明するように終了します。したがって、デストラクタは共有クライアントを使う場所としてそもそも適していません。

  3. いくつかのケースは意図的にカバーされないまま残されています。 リクエストをまたいで保持されるステートメントや結果オブジェクト。これにはクレームされた呼び出しが先行しません。以前のリクエストで prepare されたステートメント上の PDOStatement::execute() は、クレームがまったくないときと同じように振る舞い、その代償は呼び出し 1 回分では済みません。いずれかのファイバーがその接続で待機している間にソケットに到達すると、そこで拒否され、mysqlnd では拒否によって接続が失われます(上述のとおり)。これはコマンドを送る書き込みだけでなく、バッファなしの結果の次の行を取得する読み取りでも同じです。生のソケット上に手書きされたプロトコル(コマンドを書き、別の何かで中断し、後で応答を読む)。ソケットレベルが拒否するのは、保持者が応答そのもので待機している間だけであり、その 2 点の間に別のファイバーが接続を乗っ取れるからです。上の 3 つのクライアントは、手書きプロトコルには相当物のないクライアントレベルのクレームによってここでカバーされています。

    永続接続はカバーされています。後述の永続 PDO 接続を参照してください。

  4. 別のリクエストが読み取りで待機している共有接続を閉じると、そのリクエストは終了します。 500 と、何が起きたかを記したログ行つきでです。クレームは 2 つの交換を隔てておくことはできますが、接続を生かしておくことはできず、待機中のリクエストの応答は、もはや存在しない接続の上にあります — したがって、解放済みメモリが今保持しているものを返すのではなく、リクエストを終わらせるのが誠実な答えです。どの呼び出しがこれを行えるかはクライアントに依存し、待つのはそのうち 1 つだけです。mysqli::close() と mysqli_close() はクレームされる呼び出しなので、保持者を待ち、その待機が尽きたら閉じます。Redis::close() もクレームされますが、閉じる代わりに RedisException を送出します。それ以外はすべて即座に閉じます — 生のストリーム上の fclose()、そして重要なことに、close() メソッドをそもそも持たない PDO です。接続はハンドルへの最後の参照を落とすこと(unset($pdo)、再代入、スコープ外へ出すこと)で解放され、そのパスはエンジン自身のオブジェクト破棄の内側で実行され、そこでは接続が閉じられる前にクレームが一切参照されません。したがって、共有 PDO ハンドルを再代入する再接続ヘルパーは、mysqli の同等物が得る制限付きの待機なしに、古い接続で待機していたリクエストを終了させます。リクエストは、自身の読み取り期限 — mysqlnd では mysqlnd.net_read_timeout で、初期設定では 1 日 — ではなく、クローズが起きた時点ですぐに告げられます。再接続を強制するために共有接続を閉じるアプリケーション(WordPress の $wpdb->check_connection() が典型です)は、その瞬間にそこで待機していたリクエストが、誤ったデータを返すのではなく失敗することを想定しておくべきです。

もう 1 つの境界があります。ユーザーランドのファイバースケジューラ(AMPHP、Revolt)の下で streams フックを動かすと、ブロッキング I/O にフォールバックします。そのようなスケジューラが開始したファイバーは独自のコンテキスト上で実行され、OxPHP のスケジューラはそれを再開できません。そのためフックはこれを検出し、どちらのスケジューラも壊さないようネイティブのパスを取ります。これは、ユーザーランドのスケジューラがリクエストの内側で開始するファイバーの話です。リクエスト自身のファイバーは OxPHP が駆動するものであり、ワーカーモードではそれは本物の Fiber です(リクエスト内の Fiber::getCurrent() はそれを返します)。そのため、並行リクエストを区別することだけが必要なライブラリは、フォールバックなしで動きます。同じ事実が、Revolt がワーカーモードのリクエスト内からイベントループの実行を拒否する理由でもあります。

永続 PDO 接続

PDO::ATTR_PERSISTENT はドライバーの接続をワーカー全体で共有するプールに保持し、PDO はそのプールに 2 段階で到達しますが、この 2 段階は割り込まれると成り立ちません。接続する前にプールされた接続を探し、構築したものを後から登録するのです。2 つのコンストラクタが重なると、どちらもプールで見つけられず、2 回目の登録が最初のエントリを置き換え、その接続上で応答を読んでいるリクエストの下でそれを閉じてしまいます。重なるのはごく普通のことです。static $pdo ??= new PDO(...) には、最初の接続が終わる前に到着したすべてのリクエストが到達するからです。

そのため、1 つのデータソースへの永続接続を要求するコンストラクタはワーカーごとに 1 つずつ実行され、2 つ目は 1 つ目が登録したものを見つけます。データソースとは PDO が解決するもの(php.ini の pdo.dsn.* によるエイリアスは、それが指す DSN として数えられます)であり、異なるデータソースへのコンストラクタは、原則として互いを待ちません。そのため、接続を受け付けたまま一切応答しないサーバーへの接続で止まっているコンストラクタが、健全なサーバーへのコンストラクタを引き留めることはありません。待機が減るどころか増えるケースが 2 つあります。

  • コロンまで同じに読める DSN。 DSN は、ドライバープレフィックスの後の最初のコロンまでしか比較されません(IPv6 ホストはコロンを含む一例です)。PDO は DSN、ユーザー、パスワードを何もエスケープせずにコロンで連結してプールキーにするため、:… で拡張された DSN が、拡張されていないものと同じプールエントリを指しうるからです。そのコロンまで同じに読めるデータソースは、止まっているものも含めて 1 つの待機を共有します。先頭の数字グループが同じ 2 つの IPv6 ホストは、かつてワーカー上のすべてのコンストラクタがそうしていたように、互いを待ちます。
  • uri: DSN と文字列でない DSN。 これらは PDO がそれらに対して行うことを繰り返さずには解決できないため、そのようなコンストラクタは、そのワーカー上でどのデータソースへの永続コンストラクタも実行されていない状態になるまで待ち、それが実行されている間は他のすべてが待ちます。待っている間は何も保持しないため他を引き留めることはありませんが、応答しないサーバーへのコンストラクタが実行されている間はその瞬間を見つけられず、待機が尽きると直列化されないまま先へ進みます。したがって、上で述べた止まったサーバーのケースは依然として uri: やオブジェクトの DSN には及び、止まったサーバーそのものを指すものは、その接続が続く限り、そのワーカー上のすべての永続コンストラクタを以前とまったく同じように引き留めます。

この待機は上の待機と同じように制限されますが、それが尽きたときの代償はクライアントが上げるエラーではありません。予算が尽きたコンストラクタは直列化されないまま先へ進み、その旨をサーバーログに記します。そうして重なった 2 つのコンストラクタは、以前と同じように互いのプールエントリを置き換えます。これは置き換えられた接続上で応答を読んでいるリクエストを終わらせ、しかもプールされたハンドルはそれを指す PDO オブジェクトがいくつ残っていても解放されるため、後でそのオブジェクトの 1 つが破棄されたときにワーカープロセスを終わらせることがあります。ユーザーランドのファイバースケジューラ自身のファイバーの内側では、この予算は即座に使い切られます。そのようなコンテキストは中断できないため、別のコンストラクタが実行中であることに気づいたコンストラクタはすぐに諦めます。

もう 1 つの段階は、PDO がプールされた接続を渡す前に行う生存確認です。これは接続そのものに対する ping です。保持者が応答を待って待機している間に送られると拒否され、PDO は接続が死んでいると読んで 2 つ目を開き、アプリケーションが共有するつもりだったものはもはや共有されなくなります。そのため、別のファイバーが保持している接続は ping なしで生きていると報告されますが、それはその接続をまったくそのままにしておくコンストラクタに対してだけです(pdo_sqlite のようにドライバーが生存確認を提供しない場合は、同じ答えが PDO 自身の確認の代わりになります)。PDO は、渡すプールされたハンドルにエラーモード、オートコミットフラグ、その他のオプションを書き込み、使用中の接続ではそうした書き込みは保持者の上に着地してしまいます。したがって接続が共有されるのは、そこに何も新しく書き込まないコンストラクタとだけです。

  • 渡すすべてのオプションが、PDO がハンドル自体に保存するものである(PDO::ATTR_ERRMODE、PDO::ATTR_CASE、PDO::ATTR_ORACLE_NULLS、PDO::ATTR_DEFAULT_FETCH_MODE。プールを名指しする PDO::ATTR_PERSISTENT のエントリは数えません)、かつ
  • それぞれが、接続が既に持っている値を持っている。

ドライバーに伝えなければならないオプションは、値にかかわらず拒否されます。PDO::ATTR_AUTOCOMMIT と PDO::ATTR_STRINGIFY_FETCHES も含め、すべてのドライバーで、サードパーティのものでも同様です。いずれかの条件を満たさないコンストラクタには接続が死んでいると告げられ、自分専用の接続を得ます。サーバーログはそれを最大でも 1 秒に 1 回記録します。Laravel のように、オプションに PDO::ATTR_EMULATE_PREPARES や PDO::ATTR_STRINGIFY_FETCHES を含むプールキーの場合、これは次のことを意味します。リクエスト間、およびクエリの前に構築するファイバー間では共有されますが、別のリクエストが接続を保持している間に到着したコンストラクタは自分の接続を開きます。クレームは、保持者の最初のクエリからそのリクエストの終わりまで続きます。

何も使っていない接続は、保持者が最後のハンドルを落とした接続も含めて従来どおり確認されるため、本当に死んでいる接続は引き続き置き換えられます。その確認は他と同じコマンドであり、同じようにクレームされます。それが実行中の間にガードされたクライアント経由で到着したクエリは、通常の制限の範囲内でそれを待ちます。上でカバーされないとしたケースは、ここでもカバーされないままです。

有効化とコスト

1、true、all は、streams と net を含むすべてのカテゴリを有効にします。したがって、sleep フックのために既に RUNTIME_HOOKS=1 を設定しているデプロイメントは、自身では何も編集していないのに、アップグレードとともにソケットのフックを開始します。それが望みでない場合は、カテゴリを明示的に列挙してください(RUNTIME_HOOKS=sleep、あるいは 0.12.0 が有効にしていたものを維持するなら RUNTIME_HOOKS=sleep,streams)。streams を有効にすると、起動時に PHP のストリーム ops テーブルの 1 エントリがメモリ上でパッチされます。ページは見つけたときの状態に戻されますが、元の保護状態を判定できないプラットフォームでは書き込み可能なままになります — わずかなハードニングの喪失であり、サーバーログに報告されます。net を有効にすると、他のソケットトランスポートと OpenSSL の ops テーブルにある同種のエントリが同じ条件でパッチされ、ブリッジライブラリが PHP 自身のコードによる呼び出しに対して poll() に応答するようになります。サーバー自身のものを含むそれ以外のすべての呼び出し元は、そのまま libc に渡されます。

実測されたコスト: 他に何も待機していないワーカーではソケットの往復あたり約 3–5 マイクロ秒、64 個のファイバーがディスクリプタで待機している状態で約 5–6 マイクロ秒、200 個で約 7–11 マイクロ秒です。準備完了状態は、カーネルが待機と待機の間も保持するセットを通じて解決されるため、この数字が追跡するのは、いくつのディスクリプタが準備完了になったかであって、いくつが待っているかではありません。アイドル状態のワーカーは、固定間隔でスリープして次のティックで準備完了に気づくのではなく、それらのディスクリプタを待ちます。これは非常に重要です。盲目的なスリープには、代わりに往復あたりおよそ 2 ミリ秒のコストがかかっていました。

幅の広い stream_select() には、狭い場合にはないオーバーヘッドが伴います。呼び出しが名指しするすべてのディスクリプタが、待機の前に登録され、後に削除されるからです。待機そのものを除いて測定すると — すべてのディスクリプタが既に読み取り可能で、呼び出しは即座に戻り、オーバーヘッドだけが残ります — ディスクリプタ 1 個ではフックなしの 5 マイクロ秒に対して約 6 マイクロ秒、64 個では 9 に対して 56 マイクロ秒、200 個では 16–21 に対して 130–150 マイクロ秒です。おおよそディスクリプタあたり 0.65 マイクロ秒です。

これは同じ仕事の減速ではなく、呼び出しあたりの固定コストとして読んでください。実際に待つ stream_select() — select ループはそのために書かれるものです — の前ではこれは霞みます。1 ミリ秒の待機があれば、200 ディスクリプタの数字でさえ呼び出しの約 1 割にすぎず、それが買い戻すのはワーカースレッドです。フックなしの呼び出しは待機の間ずっとそれを占有します。例外となる形が 2 つあり、それらではこのカテゴリはオフのままにしておく方が良いでしょう。ほとんど待つことのない多数のディスクリプタにわたる呼び出し — 回収できるスレッド時間がないからです — と、リクエスト自体がイベントループである場合 — スレッドにはどのみち他に実行するものがないからです。1 つの接続を待つクライアント — mysqlnd、phpredis、HTTP ストリームラッパー — は表の最上部に位置し、そこではオーバーヘッドは 1 マイクロ秒前後です。

RUNTIME_HOOKS と oxphp_sleep() の比較

このフックはファーストパーティのプリミティブを置き換えるものではありません — 両者は異なるケースをカバーします。

  • 自分で書くコードでは oxphp_sleep() / oxphp_usleep() に手を伸ばしてください。これらは設定すべき環境フラグなしにワーカーモードでファイバーを中断し、oxphp_sleep() は小数秒(oxphp_sleep(0.25))を受け付けます。ネイティブの sleep()(整数秒)には表現できない精度です。これらもファイバーが中断できない場所の例外ではなく、そこではフックされた組み込み関数とまったく同じようにワーカースレッドを保持します。
  • 編集できないコード、たとえばネイティブの sleep()/usleep() を直接呼び出すフレームワークやベンダーライブラリのためには、RUNTIME_HOOKS=sleep を有効にしてください。デフォルトでは無効で、ファイバーの外側では何もせず、ネイティブの戻り値の契約を保ちます。フックされた sleep() は依然として int を取り、中断したときは 0 を、フォールバックしたときは組み込み関数が返すものを返します。引数チェックは上述のとおりフック自身のものです。

自分のハンドラーで RUNTIME_HOOKS に頼ると、その協調性がコードではなくデプロイメント設定に結び付けられます。そこでは明示的な oxphp_sleep() を優先してください。

共有状態

プロセス内の並行処理プリミティブ(OxPHP\Shared\Counter、Map、Channel、Mutex、Once、Pool、Atomic、Flag、Registry)。API の概要は共有状態を参照してください。

変数 デフォルト 説明
SHARED_ENABLED true ブール値 — ブール値を参照。OxPHP\Shared\* サブシステム全体のマスタースイッチ。false に設定すると、そのクラスは 1 つも登録されません。new OxPHP\Shared\Counter() は Error: Class "OxPHP\Shared\Counter" not found をスローし、/__ox_shared/* と oxphp_shared_* メトリクスは存在しなくなります。どちらの場合でも動かなければならないコードでは class_exists('OxPHP\Shared\Counter') でガードしてください。interface_exists('OxPHP\Shared\Shareable') ではガードしないでください。拡張はそのインターフェースを常に登録するため、true のままです
SHARED_MAX_ENTRIES 100000 生存している Shared オブジェクトの数のグローバルな上限(Counter、Map、Channel などはそれぞれ 1 つと数えます)。これを超えてオブジェクトを作成すると CapacityException で失敗します。Map 内のキーはこれに数えられません。Map は maxEntries で制限してください
SHARED_MAX_BYTES 1073741824(1 GiB) すべての Shared オブジェクトにわたる推定メモリのグローバルな上限。チェックされるのはオブジェクトの作成時だけです(CapacityException)。既に存在するオブジェクトはこれを超えて成長し続けられるため、合計が上限を上回って見えることがあります
SHARED_SOFT_LIMIT_RATIO — 非推奨で無視されます。設定すると起動時に WARN を出力します。これは一度も効果を持ったことがありません。使用量が SHARED_MAX_BYTES / SHARED_MAX_ENTRIES に近づいても何も切り捨てられず、これらの上限は到達時に新しい Shared オブジェクトの作成を拒否するだけです。早期警告には、SHARED_METRICS_ENABLED が有効な間に公開される oxphp_shared_capacity_saturation でアラートを設定してください(Shared* オブザーバビリティを参照)
SHARED_METRICS_ENABLED true ブール値。oxphp_shared_* の Prometheus エクスポジションを切り替えます
SHARED_INTROSPECTION_ENABLED true ブール値。内部サーバー上の /__ox_shared/* イントロスペクション API を切り替えます
SHARED_INTROSPECTION_PREVIEW_ENABLED true ブール値。イントロスペクションのレスポンスでの値のプレビューを切り替えます(プレビューが機密データを漏らすおそれがある場合は無効にします)
SHARED_CYCLE_DETECT_DEPTH 16 循環チェック時の BFS の深さ。正当に深いグラフの場合は引き上げてください
SHARED_CYCLE_DETECT_EDGES 10000 循環チェック時に走査するエッジ数。正当に密なグラフの場合は引き上げてください
SHARED_MAX_VALUE_SIZE 1048576(1 MiB) 値ごとのサイズ上限。これより大きい値の挿入は即座に失敗します
SHARED_MAX_CHANNEL_BYTES 67108864(64 MiB) チャネルごとの合計ペイロード上限
SHARED_POISON_STRICT — 非推奨で無視されます。設定すると起動時に WARN を出力します。これは一度も効果を持ったことがなく、設定対象となるグローバルな汚染(poison)ポリシーも存在しません。Shared\Mutex のクロージャ内でスローされた例外がミューテックスを汚染することは決してなく、失敗した Shared\Once のファクトリがセルを恒久的に無効化するかどうかはインスタンスごとに選びます。コンストラクタに Once\FailureMode::Poison を渡してください(Shared\Onceを参照)
SHARED_LOCK_DIAGNOSTICS warn(バイナリのデバッグビルドでは strict) Shared\Mutex のためのスレッド間デッドロック検出器: off、warn(循環をログに記録してカウント)、または strict(さらに DeadlockException で循環を断ち切る)。小文字のみ。それ以外の値は黙ってデフォルトになります。デッドロック検出器を参照
SHARED_LOCK_POLL_INTERVAL_MS 100 デッドロック検出器がスキャンする間隔(ミリ秒)。SHARED_LOCK_DIAGNOSTICS=off のときは使われません
SHARED_PREVIEW_STRING_LIMIT 256 /__ox_shared/preview プレビューでの文字列ごとの切り詰め。バイト単位(文字境界で切られます)
SHARED_PREVIEW_ARRAY_LIMIT 20 /entry?id=… プレビューでサンプリングするエントリ数

プロファイリング

xhprof / speedscope のトレースを出力するサンプリングプロファイラー。出力形式とビューアー連携についてはプロファイリングを参照してください。

変数 デフォルト 説明
PROFILER_ENABLED false ブール値 — ブール値を参照。マスタースイッチ。他のすべての PROFILER_* 変数も起動時に解析されるため、タイプミスは即座に表面化します
PROFILER_SAMPLE_RATE 0.0 リクエストがサンプリングされる確率(0.0–1.0)。範囲外の値はクランプされます
PROFILER_INTERNAL false ブール値。真値のとき、内部サーバー(/health、/metrics、プラグインエンドポイント)へのリクエストもサンプリングの対象になります
PROFILER_AUTH_TOKEN (未設定) 明示的なトリガー(X-OxPHP-Profile ヘッダー、OXPROF Cookie、__oxprof クエリパラメータ)が持たなければならないシークレットであり、内部サーバー上の /__profiler/* ルートのベアラートークンでもあります。未設定または空 = トークン不要。空でないトリガー値ならどれでも通り、/__profiler/* ルートはベアラーなしで応答します。PROFILER_SAMPLE_RATE のサンプリングと OxPHP\Profile\* PHP 関数はこれを一切参照しません。PROFILER_ENABLED=true のもとでは、OxPHP\Profile\start() を呼び出すスクリプトは、トークンとは無関係に自分のリクエストをプロファイルします
PROFILER_MAX_SPANS 50000 リクエストごとのプロファイルスパンの上限。上限を超えるプロファイルは切り詰められます
PROFILER_MAX_DEPTH 0(上限なし) 記録される呼び出しの最大ネスト深さ。それより深い呼び出しと、32 フレームを超える自己再帰の場合を除きそれが呼び出すものすべては、プロファイルに現れません。ただしその時間は失われず、上限の位置にある呼び出しの排他時間として現れます。落とされた呼び出しも PROFILER_MAX_SPANS に数えられます。エンジンのオブザーバーが追跡する深さである 32 にクランプされます。PROFILER_ENABLED=true のとき、それより大きな値は起動時にクランプされた旨が報告されます
PROFILER_OUTPUT_DIR /tmp/oxphp-profiles ディスク上のプロファイルファイル用のディレクトリ
PROFILER_OUTPUT_FORMATS xhprof,speedscope ディスクに書き込む出力形式のカンマ区切りリスト
PROFILER_DISK_MAX_PER_SEC 10 1 秒あたりにディスクへ書き込むプロファイルファイル数のレート制限
PROFILER_RETENTION_COUNT 100 PROFILER_OUTPUT_DIR に保持するプロファイルファイルの最大数。古いファイルは削除されます
PROFILER_EXPORT_URL (未設定) プロファイルを POST するリモートエンドポイント。設定すると、PROFILER_OUTPUT_FORMATS が空でない限りディスク書き込みも引き続き行われます
PROFILER_EXPORT_FORMAT xhprof PROFILER_EXPORT_URL への POST に使うワイヤーフォーマット
PROFILER_EXPORT_AUTH_TOKEN (未設定) 各エクスポートリクエストとともに送られるオプションのベアラートークン
PROFILER_EXPORT_XHGUI (自動検出) ブール値。エクスポートペイロードを XHGui 互換でラップするよう強制します。未設定 = PROFILER_EXPORT_URL のパスが /run/import で終わるときに自動検出(ホストやクエリのヒントはマッチ対象外)
PROFILER_EXPORT_BUGGREGATOR (自動検出) ブール値。Buggregator のエンベロープを強制します。未設定 = PROFILER_EXPORT_URL のパスが /api/profiler/store で終わるときに自動検出。エンベロープは常に xhprof を出力するため、PROFILER_EXPORT_FORMAT はこの場合無視されます(xhprof 以外の値は警告を出しますが致命的ではありません)。PROFILER_EXPORT_XHGUI とは相互排他で、両方を有効にすると起動エラーになります
PROFILER_EXPORT_APP_NAME (未設定) プロジェクトのグルーピング用の Buggregator の app_name
PROFILER_EXPORT_TAGS (未設定) key=value,key2=value2 形式の Buggregator の tags。不正な形式のトークン、空のキー、重複するキーは起動エラーになります

設定例

開発

bash
LISTEN_ADDR=127.0.0.1:8080 DOCUMENT_ROOT=./public LOG_LEVEL=debug ACCESS_LOG=all PHP_WORKERS=1 INTERNAL_ADDR=127.0.0.1:9090

本番(Framework)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php PHP_WORKERS=8 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error MAX_CONNECTIONS=10000 INTERNAL_ADDR=127.0.0.1:9090 RATE_LIMIT=100 RATE_WINDOW_SECONDS=60 TRUSTED_PROXIES=private HEADER_TIMEOUT_SECONDS=5 DRAIN_TIMEOUT_SECONDS=25 COMPRESSION_BROTLI_LEVEL=5 COMPRESSION_ZSTD_LEVEL=6 COMPRESSION_GZIP_LEVEL=6 STATIC_MAX_AGE=30d

本番(ワーカーモード)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public WORKER_MODE_ENABLED=true ENTRY_FILE=../worker.php PHP_WORKERS=8 WORKER_MAX_MEMORY_MIB=128 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error INTERNAL_ADDR=127.0.0.1:9090

TLS

bash
LISTEN_ADDR=0.0.0.0:443 TLS_CERT=/etc/ssl/oxphp/cert.pem TLS_KEY=/etc/ssl/oxphp/key.pem DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php

稼働中の設定を確認する

内部サーバーが稼働しているとき、/config エンドポイントに問い合わせると、解決済みの設定を確認できます。

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": {"loaded": false}, "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": "warn", "max_connections": 10000, "max_query_body": 524288, "php_workers": "8", "plugins": { "apm": { "db_capture_params": false, "enabled": true, "hooks_registered": 34, "slow_query_ms": 100 }, "otel": { "enabled": true, "protocol": "grpc", "service_name": "oxphp" } }, "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": [], "static_max_age": 2592000, "static_revalidate": false, "superglobals_enabled": true, "tls_enabled": true, "tls_min_version": "1.2", "tokio_workers": 4, "trace_context": true, "trusted_proxies": false, "worker_max_memory_mib": 0, "worker_mode_enabled": false, "zstd_level": 6 }
Note

配信される /config レスポンスは、内部の Config 表現が保持しているいくつかのキーを取り除きます。TLS 証明書と鍵のパスは決して出力されず(tls_enabled が TLS が有効かどうかを示します)、internal_addr と error_pages_dir も削除されます — これらは攻撃者の助けとなり、メトリクススクレイパーには不要な、デプロイのトポロジーとファイルシステムのパスだからです。

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

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

prompt
私のプロジェクト向けに OxPHP をチューニングしてください。OxPHP ドキュメント MCP サーバー経由で `operations/configuration` ページを読んでください。 このリポジトリが何を動かしているのか — フレームワーク、確保できるワーカー数、処理するリクエストの形 — を見たうえで、本番環境向けの環境変数ブロックを、選んだ値それぞれの理由を添えて教えてください。
prompt
私の OxPHP の設定をレビューしてください。OxPHP ドキュメント MCP サーバー経由で `operations/configuration` ページを読んでください。 このリポジトリが設定している環境変数をデフォルト値と比較して、私の上書きのうち何も効いていないものはどれか、危険なものはどれか、そして上書きすべきだったのにしていないデフォルト値はどれかを教えてください。

関連項目