グレースフルシャットダウン
OxPHP は SIGTERM と SIGINT を処理し、処理中のリクエストが完了してからプロセスを終了します。これはコンテナオーケストレーションにおけるゼロダウンタイムのデプロイやローリングアップデートで重要になります。
シグナルの処理
OxPHP は 2 つのシャットダウンシグナルに応答します。
| シグナル | 発生源 | 挙動 |
|---|---|---|
SIGTERM |
コンテナオーケストレーター、docker stop、kill |
グレースフルシャットダウンを開始 |
SIGINT |
ターミナルの Ctrl+C | グレースフルシャットダウンを開始 |
どちらのシグナルも同じシャットダウンシーケンスをトリガーします。必要なのは最初のシグナルだけです。サーバーはただちにドレインを開始します。
シャットダウンシーケンス
シャットダウンシグナルを受信すると、OxPHP は次のシーケンスに従います。
- 新しい接続の受け付けを停止する: サーバーはメインポートでの接続の受け付けを停止しますが、リッスンソケット自体はプロセスが終了するまで開いたままです。そのため、ドレイン中に接続してきたクライアントは TCP ハンドシェイクを完了したまま応答を受け取れず、自身のタイムアウトに達するか、プロセスが終了して接続がリセットされるまで待ち続けます。即座に拒否されるのはプロセス終了後の接続だけなので、シグナルがインスタンスに届く前にインスタンスをローテーションから外してください(Kubernetes を参照)。PHP ワーカーは処理中のリクエストを処理するために動作を続けます。
- 稼働中の接続を段階的に終了する — HTTP/2 クライアントには
GOAWAYフレームが送られ、アイドル状態の HTTP/1.1 keep-alive 接続は閉じられます。これにより、クライアントは終了しつつあるインスタンスに新しいリクエストを多重化するのではなく、正常なインスタンスへ移動します。オープン中のストリームは速やかかつクリーンに終了されます。チャンク出力のフラッシュを開始したレスポンスはすべて対象で、有限サイズのダウンロードも SSE も含まれます。Server-Sent Events を参照してください。 - 処理中のリクエストをドレインする — 通常の実行中リクエストはそのまま残され、完全なレスポンスを返して完了できます。サーバーは 100ms ごとに完了を確認します。内部のヘルス/メトリクスサーバーはドレインの間ずっと利用可能なままなので、readiness プローブは引き続き機能します。
oxphp_finish_request()の後もリクエストが続けている処理は、接続が既に失われていても処理中としてカウントされます。 - ドレインの期限を強制する:
DRAIN_TIMEOUT_SECONDSを過ぎてもまだ実行中のリクエストはキャンセルされ(そのregister_shutdown_function()コールバックは引き続き実行されます)、サーバーが処理を進める前に後始末のために約 2 秒が与えられます。ブロッキング呼び出し(sleep()、データベースクエリ、ソケットの読み取り)の中にあるリクエストは、その場で中断できません。これはトラディショナルモードでは常に当てはまり、ワーカーモードでもRUNTIME_HOOKSがその呼び出しをファイバーのサスペンドに変えていない限り当てはまります。その接続は予定どおり閉じられますが、ワーカースレッド、ひいてはプロセスが終了するのは呼び出しが戻ってからです。したがって約 2 秒という値は、プロセスが終了する時刻の上限ではありません。 - プラグインをフラッシュする — ドレインウィンドウの間にバッファされたアクセスログのエントリと APM スパンがフラッシュされます。
- 非同期プールをシャットダウンする — バックグラウンドの非同期タスクプールが停止されます。
- 内部サーバーを中断する — ドレインの完了後にヘルス/メトリクスサーバーが停止されます。
- 終了する — プロセスはステータスコード 0 で終了します。
ステップ 1 と 2 はシグナルが届いた瞬間に始まりますが、ドレインの期限のカウントが始まるのは accept ループが停止してからです。シグナルの到着時に MAX_CONNECTIONS で許されるすべての接続が使用中であれば、ループはいずれかの接続が閉じるのを待っており、停止するのはそのときです。接続が閉じるまでは期限のカウントは始まらず、まだ実行中の処理がステップ 4 でキャンセルされることもありません。アイドル状態の keep-alive 接続が上限を占めている場合は、ステップ 2 がそれらを閉じるのでこの状態はすぐに解消されます。長時間のリクエストや低速なクライアントが占めている場合は、それらが続く限りこの状態も続き、オーケストレーターの猶予期間をすべて使い切ることもあります。
PHP ワーカースレッドは、シーケンスの冒頭では停止されません。終了のシグナルを受けて join されるのは一番最後、ドレインが完了してプロセスが終了へ向かう段階です。だからこそドレインは、クライアント接続がもう残っていなくても、ワーカー上でまだ実行中の処理を考慮に入れなければならないのです。
設定
| 変数 | デフォルト | 説明 |
|---|---|---|
DRAIN_TIMEOUT_SECONDS |
25 |
処理中のリクエストがキャンセルされるまでに完了できる最大秒数。サーバーは期限の約 2 秒後にサービスを停止し、ブロッキング呼び出しの中にあるリクエストがない限り、プロセスもその時点で終了します(上記のステップ 4 を参照)。デフォルト値は、期限後の後始末とテレメトリのフラッシュのための余裕を、Kubernetes のデフォルトの 30 秒の終了猶予期間の中に残しています |
DRAIN_TIMEOUT_SECONDS は、想定される最も遅いリクエストに合わせて設定してください。
- 高速なレスポンスを返す API サーバー:
10〜15秒 - ファイルアップロードや長時間のクエリを伴う アプリケーション:
30〜60秒 - バックグラウンド処理を行う ワーカーモード: 想定される最も長い処理に合わせる
Kubernetes
Kubernetes におけるローリングアップデート中のシャットダウンフローは次のとおりです。
- Kubernetes が Pod に
SIGTERMを送信します。 - Pod が Service のエンドポイント一覧から削除されます。
- OxPHP が
DRAIN_TIMEOUT_SECONDS以内に処理中の接続をドレインし、その後で残っているものをキャンセルして、約 2 秒後にサービスを停止します。プロセスはその時点で、あるいはワーカー上の最後のブロッキング呼び出しが戻った時点で終了します。 terminationGracePeriodSecondsを過ぎても Pod がまだ実行中の場合、Kubernetes はSIGKILLを送信します。
terminationGracePeriodSeconds は DRAIN_TIMEOUT_SECONDS + 2 に、期限を過ぎた時点でリクエストが入っている可能性のある最長のブロッキング呼び出し(アプリケーションが許す最長のクエリまたはソケットのタイムアウト)を加えた値より大きく設定し、ドレイン(期限後の後始末とテレメトリのフラッシュを含む)が強制終了の前に完了するようにしてください。+ 2 だけでは、期限の時点で長いクエリの中にあるリクエストがあると、Pod は 0 で終了する代わりに SIGKILL(終了コード 137)で強制終了されます。
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 45
containers:
- name: oxphp
image: ghcr.io/oxphp/oxphp:0.13.0
env:
- name: DRAIN_TIMEOUT_SECONDS
value: "30"pre-stop フック
外部のロードバランサーからトラフィックを受け取っており、そのエンドポイントの変更の伝播が遅い場合は、pre-stop フックを追加してシャットダウンシーケンスを遅延させてください。
lifecycle:
preStop:
exec:
command: ["sleep", "5"]これにより、OxPHP が接続の受け付けを停止する前に、ロードバランサーが自身のターゲット一覧から Pod を削除する時間が確保されます。これが重要なのは、シグナルの後に Pod へルーティングされた接続は拒否されるのではなく、ドレインが終わるまで応答のないまま放置されるからです(シャットダウンシーケンスのステップ 1 を参照)。
Docker
docker stop を実行すると、Docker は SIGTERM を送信します。Docker の停止タイムアウトのデフォルトは 10 秒で、それを過ぎると Docker は SIGKILL を送信します。
OxPHP にドレインのための十分な時間を与えるため、停止タイムアウトを延長してください。
docker stop --time 45 my-oxphp-containerあるいは Compose ファイルで設定します。
services:
oxphp:
image: ghcr.io/oxphp/oxphp:0.13.0
stop_grace_period: 45s
environment:
DRAIN_TIMEOUT_SECONDS: "30"ログメッセージ
グレースフルシャットダウンの間、OxPHP は監視可能な構造化ログメッセージを出力します。
ドレインの成功時:
{"level":"INFO","message":"Received shutdown signal, draining connections"}
{"level":"INFO","message":"Draining in-flight connections","active_connections":3,"in_flight_requests":4}
{"level":"INFO","message":"All connections drained"}
{"level":"INFO","message":"Server stopped"}2 つのカウントは分割ではなく重なり合います。active_connections は稼働中のクライアント接続、in_flight_requests は PHP ワーカー上でまだ実行中のリクエストであり、接続を保持しているリクエストは両方にカウントされます。oxphp_finish_request() でレスポンスを早期に終えたリクエストは in_flight_requests にのみ現れます。バックグラウンド処理が動いている間、その接続は既に失われているからです。
ドレインの期限に達した時:
{"level":"INFO","message":"Received shutdown signal, draining connections"}
{"level":"WARN","message":"Drain timeout reached, cancelling in-flight requests","remaining_connections":1,"in_flight_requests":1}
{"level":"INFO","message":"All connections drained"}
{"level":"INFO","message":"Server stopped"}「Drain timeout reached」の警告が頻繁に見られる場合は、DRAIN_TIMEOUT_SECONDS を増やすか、oxphp_request_duration_us ヒストグラムを使って長時間実行されているリクエストを調査してください。
エージェント向けプロンプト
これらのプロンプトは、OxPHP ドキュメント MCP サーバー を接続したエージェントを前提としています — 必要なページはエージェントが自分で読み取ります。
私の OxPHP のデプロイを、リクエストを取りこぼさずにシャットダウンできるようにしてください。OxPHP ドキュメント MCP サーバー経由で `operations/graceful-shutdown` ページを読んでください。
私の Kubernetes マニフェストの終了猶予期間、preStop フック、シャットダウンのタイムアウトを、私のアプリケーションが許す最長のブロッキング呼び出し(クエリまたはソケットのタイムアウト)を考慮したうえで互いに整合するように設定し、各ステップで処理中のリクエストがどうなるのかを教えてください。