Shared* オブザーバビリティ

すべての OxPHP\Shared\* インスタンスは、ランタイムが refcount と容量のためにすでに追跡しているレジストリエントリです。その追跡内容は、/__ox_shared/* 配下の JSON イントロスペクションと、oxphp_shared_* 配下の Prometheus メトリクスとして運用者に公開されます。このページはそのリファレンスであり、実践ガイドでもあります。

有効化

オブザーバビリティは内部サーバーに相乗りしています。起動するには INTERNAL_ADDR を設定します。

bash
INTERNAL_ADDR=127.0.0.1:9090

これで JSON エンドポイントと /metrics の両方が、そのアドレスで到達可能になります。追加の設定は不要です。

どちらも個別に無効化できます。

環境変数 デフォルト 効果
SHARED_INTROSPECTION_ENABLED true /__ox_shared/* の JSON API を切り替えます。
SHARED_INTROSPECTION_PREVIEW_ENABLED true /preview を切り替えます(値の形状プレビューはデータを漏らす可能性があります)。
SHARED_METRICS_ENABLED true oxphp_shared_* の Prometheus メトリクスを切り替えます。

敵対的なテナントが同居するデプロイではイントロスペクションを無効にしてください。メトリクスは集計値のみなので、有効なままにしておいても安全です。

イントロスペクションエンドポイント

すべてのレスポンスは Content-Type: application/json; charset=utf-8 です。クエリパラメータは標準の URL エンコードです。

GET /__ox_shared/summary

トップレベルのスナップショット。型ごとの集計数、メモリ、オペレーションレート、そして設定された上限に対する飽和度が得られます。

json
{ "total_entries": 127, "total_bytes": 2_481_664, "by_type": { "Counter": { "count": 48, "bytes": 3_072, "ops": 1_402_391 }, "Map": { "count": 12, "bytes": 1_638_400, "ops": 48_201 }, "Pool": { "count": 4, "bytes": 16_384, "ops": 67_014 } }, "limits": { "max_entries": 100_000, "max_bytes": 1073741824, "soft_ratio": 0.7 }, "saturation": { "entries": 0.00127, "bytes": 0.00231 }, "diagnostics": { "lock_diagnostics_level": "warn", "cycle_detect_depth": 16, "poison_strict": false } }

ダッシュボードや cron アラートでは summary を使ってください。1 回のスクレイプで、型ごとの健全性と容量の余裕が得られます。

GET /__ox_shared/entries?limit=N

ライブのエントリを一覧します(limit で上限を指定、デフォルト 100、最大 500)。1 エントリにつき 1 行です。

json
{ "items": [ { "id": 42, "type": "Map", "refcount": 2, "ops": 1820, "mem_bytes": 204_800, "age_sec": 612 }, { "id": 43, "type": "Counter", "refcount": 3, "ops": 48_014, "mem_bytes": 64, "age_sec": 612 } ], "next_cursor": null, "total_matching": 127 }

refcount は外部の保持カウントです。つまり、いくつの PHP ラッパーとネストされた Shared エントリがこのエントリを保持しているかを表します。あるエントリが GC 可能になるはずなのにならない場合は、このフィールドを確認します。

GET /__ox_shared/entry?id=N

1 つのエントリに対する型ごとの詳細です。

json
{ "id": 42, "type": "Map", "refcount": 2, "ops": 1820, "mem_bytes": 204_800, "age_sec": 612, "type_specific": { "key_count": 1_240, "max_entries": 50_000, "saturation": 0.0248, "sample_keys": ["tenant:acme", "tenant:beta", "..."] } }

type_specific は型によって異なります。Pool は { size, in_use, idle, waiting, idle_by_thread, max_size } を、Channel は { capacity, pending, closed, senders_blocked, receivers_blocked } を、Counter は { value } を公開する、といった具合です。

GET /__ox_shared/preview?id=N

スカラー値および小さな配列値の、値の形状プレビューです。文字列値は SHARED_PREVIEW_STRING_LIMIT(デフォルト 256 バイト)に切り詰められ、配列は先頭 SHARED_PREVIEW_ARRAY_LIMIT 件(デフォルト 20)のエントリを表示します。SHARED_INTROSPECTION_PREVIEW_ENABLED によって制御されます。

json
{ "id": 42, "type": "Counter", "preview": "1420" }

preview は開発中に使ってください。値にユーザーデータが含まれる可能性がある本番環境では無効にします。

GET /__ox_shared/types

v1 の型カタログを列挙します。tag → class のマッピングを必要とする自動生成ツールに便利です。

json
{ "types": [ { "tag": 10, "name": "Counter", "php_class": "OxPHP\\Shared\\Counter" }, { "tag": 11, "name": "Flag", "php_class": "OxPHP\\Shared\\Flag" }, { "tag": 12, "name": "Once", "php_class": "OxPHP\\Shared\\Once" }, { "tag": 20, "name": "Map", "php_class": "OxPHP\\Shared\\Map" }, { "tag": 30, "name": "Mutex", "php_class": "OxPHP\\Shared\\Mutex" }, { "tag": 31, "name": "Channel", "php_class": "OxPHP\\Shared\\Channel" }, { "tag": 50, "name": "Pool", "php_class": "OxPHP\\Shared\\Pool" } ] }

GET /__ox_shared/graph?id=N[&depth=D][&edges=E]

id=N を起点として、外向きの Shareable 参照を BFS で辿ります。到達可能な部分グラフのノードとエッジを返します。デフォルトは depth=16edges=500 です。ウォーカーの予算に達すると、レスポンスで truncated: true が設定されます。

json
{ "root": 42, "nodes": [ { "id": 42, "type": "Map", "refcount": 2, "mem_bytes": 204_800 }, { "id": 51, "type": "Counter", "refcount": 1, "mem_bytes": 64 } ], "edges": [ { "from": 42, "to": 51, "key": "hits" } ], "truncated": false }

CycleException の後にウォーカーが辿った到達可能なパスを確認したいときや、「なぜこの Counter は GC されないのか」を診断するときには graph を使ってください。グラフは、そのエントリに対して保持を持つすべての親を示します。

Prometheus メトリクス

すべてのメトリクスは、コアサーバーのメトリクスと並んで GET /metrics で公開されます。

レジストリ全体

メトリクス ラベル 説明
oxphp_shared_objects_total gauge type 型ごとのライブエントリ数。
oxphp_shared_operations_total counter type 各型にディスパッチされた累積オペレーション数。
oxphp_shared_bytes gauge type 型ごとの概算バイト数(mallinfo に対して ±30%)。
oxphp_shared_total_bytes gauge 全型の合計。
oxphp_shared_capacity_saturation gauge kind 上限に対する割合としての entriesbytes
oxphp_shared_deadlock_detected_total counter 検出されたスレッド間の wait-for サイクル数。

Channel

メトリクス ラベル
oxphp_shared_channel_count gauge channel_id
oxphp_shared_channel_pending (非推奨) gauge channel_id
oxphp_shared_channel_senders_blocked gauge channel_id
oxphp_shared_channel_receivers_blocked gauge channel_id
oxphp_shared_channel_items_sent_total counter channel_id
oxphp_shared_channel_items_dropped_total counter channel_id
Note

oxphp_shared_channel_pendingoxphp_shared_channel_count の旧称です。非推奨期間中は両方の系列が 同じ値を持ちますが、将来のリリースでエイリアスが削除されると 乖離します。新しいダッシュボードは _count を対象にしてください。

Map

メトリクス ラベル
oxphp_shared_map_entries gauge map_id
oxphp_shared_map_max_entries gauge map_id
oxphp_shared_map_saturation gauge map_id

Pool

メトリクス ラベル
oxphp_shared_pool_count gauge pool_id
oxphp_shared_pool_size (非推奨) gauge pool_id
oxphp_shared_pool_in_use gauge pool_id
oxphp_shared_pool_idle gauge pool_id
oxphp_shared_pool_waiting gauge pool_id
oxphp_shared_pool_acquire_total counter pool_id
oxphp_shared_pool_evicted_total counter pool_id, reason
oxphp_shared_pool_wait_seconds histogram pool_id
Note

oxphp_shared_pool_sizeoxphp_shared_pool_count の旧称です。非推奨期間中は両方の系列が 同じ値を持ちますが、将来のリリースでエイリアスが削除されると 乖離します。新しいダッシュボードは _count を対象にしてください。

oxphp_shared_pool_evicted_total のラベル: reason=idle_timeout | evict | shutdownidle_timeout はアイドルスロットの自動エビクション、evict は明示的な Pool::evict() 呼び出し、shutdown はプロセス終了時のティアダウンです。

Counter / Flag / Once / Mutex

インスタンスごとの Counter、Flag、Once、Mutex は、個別のメトリクス系列を提供しません。ラベルのカーディナリティが肥大化してしまうためです。インスタンスごとの調査には、代わりにレジストリ全体の oxphp_shared_operations_total{type=...} カウンターと /__ox_shared/entry?id=… の JSON を使ってください。

Mutex メトリクスは v1.x の候補

フォローアップ作業として追跡しています。現時点での可視性は /__ox_shared/entry 経由です。

診断プレイブック

Pool が飽和している(リトライが失敗する 429)

症状: HTTP の呼び出し元がタイムアウトを受け取り、oxphp_shared_pool_waiting が上昇し、oxphp_shared_pool_countmaxSize に張り付きます。

確認:

bash
curl -s http://localhost:9090/__ox_shared/entry?id=<pool_id> | jq .type_specific

idle_by_thread を確認します。これが {} であるか、大きく偏っている場合(ワーカー 0 に 8 個のアイドル、ワーカー 3 に 0 個)、acquire は、たまたま別の場所でビジーになっているスレッドを奪い合っています。v1 のスレッドごとのアフィニティはリバランスしません。maxSize を引き上げるか、スレッドごとの acquire のホットスポットを減らしてください。

idle_by_thread が均衡しているのにすべてが in_use になっている場合は、maxSize を引き上げてください。

メモリの飽和

oxphp_shared_total_bytesoxphp_shared_capacity_saturation{kind="bytes"} を確認します。どちらかが高い場合:

  1. curl /__ox_shared/entries?limit=500 を実行し、mem_bytes でソートして、上位の要因を見つけます。
  2. それぞれに対して curl /__ox_shared/entry?id=<N> を実行して形状を確認します。Map の場合は key_countmax_entries を見ます。
  3. 最も一般的な原因: ユーザー入力をキーとする、上限のない Shared\Map。対処法は maxEntries の上限と保持ポリシーです。

ラッパーがガベージコレクトされない

/__ox_shared/entriesrefcount は、未解放の保持がいくつあるかを教えてくれます。PHP ラッパーがスコープを抜けた後も 1 を超えたままの場合、別の Shared エントリがそれを生かし続けています。

bash
curl -s http://localhost:9090/__ox_shared/graph?id=<N> | jq .nodes

グラフを逆向きに辿ります。行き詰まったエントリに到達するノードはいずれも保持を持っています。参照を取り除けば($map->remove($key)、チャネルのクローズ、Mutex エントリの破棄)、refcount は下がります。

本番環境で CycleException が発生した

例外のメッセージには、サイクル検出器が探索した到達可能なパスが含まれています。それらの ID を /__ox_shared/entries で型に対応付け、全体の形状は /__ox_shared/graph?id=<root> に問い合わせます。

bash
# Exception message: "cycle would form: #42 → #51 → #42" curl -s http://localhost:9090/__ox_shared/graph?id=42 | jq

結果はチェーンを可視化するので、意図しない逆参照がどこで導入されたかを確認できます。

デッドロック検出器が発火した

oxphp_shared_deadlock_detected_total が増加しています。サーバーログを確認してください。検出器は、関与している mutex ID と所有スレッドを含むログレコードをサイクルごとに出力します。復旧するには:

  1. それぞれに対して curl /__ox_shared/entry?id=<mutex_id> を実行し、poisoned=false を確認します。ポイズニングされている場合、検出器はすでにそのサイクルを中断しています。
  2. サイクルが本当の再入バグである場合は、ロックスコープごとに別々の mutex を使うようリファクタリングします。
  3. ステージングで SHARED_LOCK_DIAGNOSTICS=strict に引き上げると、今後の再入は、検出されるサイクルではなく即時失敗(fast-fail)になります。

長時間ソークハーネス

tests/soak/pool_soak.sh は、数時間から数日にわたる継続的な負荷をかけて Shared\Pool の安定性を検証するための、手動(非 CI)ハーネスです。このハーネスは次のことを行います。

  1. 動的なワーカースケーリング(デフォルトで PHP_WORKERS=4:40)と短いプール idleTimeout を指定して dev イメージを起動し、エビクションスケジューラーが継続的に発火するようにします。
  2. tests/soak/workload.php をワーカーのブートストラップとして読み込みます。これは 10 個のプール × maxSize=8 を構築し、リクエストごとに acquire/release を処理します。
  3. wrkSOAK_DURATION_MIN 分間(デフォルト 1440 = 24 時間)トラフィックを流します。
  4. /metrics とコンテナの RSS を 60 秒ごとにスクレイプして tests/soak/out/<timestamp>/metrics.csv に記録します。
  5. 最後に、5 つのリリース終了基準(RSS のドリフトが ±5% 以内、stale-handle パニックがゼロ、シャットダウン時のリークエントリがゼロ、idle-timeout エビクションが滑らかに増加、デッドロック検出器の発火がゼロ)に対する合否を verify.txt に書き出します。

ホスト側の前提条件: dockerwrkcurlawk

典型的な実行例:

bash
# 24h full soak before a release tests/soak/pool_soak.sh # 1h smoke for validating the harness itself SOAK_DURATION_MIN=60 tests/soak/pool_soak.sh # Heavier concurrency SOAK_CONCURRENCY=400 SOAK_THREADS=8 tests/soak/pool_soak.sh

成果物は tests/soak/out/<timestamp>/ に出力されます。

  • metrics.csv — 1 分ごとに 1 行(unix タイムスタンプ、RSS、型ごとのエントリ数、プールごとのエビクションカウンター、デッドロック数、オペレーション数)。
  • server.log — stale-handle やパニックの痕跡を含む、コンテナの stdout/stderr。
  • wrk.out / wrk.err — 負荷生成ツールの生の出力。
  • metrics.final — コンテナのティアダウン直前に取得した最後の /metrics スクレイプ。負荷が止まった後にエントリ数とバイト数がベースラインに戻ったこと(Shared エントリのリークがないこと)を確認するために使います。
  • verify.txt — 5 つの終了基準に対する合否レポート。
Warning

これを CI に組み込まないでください。24 時間の実行は安くはなく、その目的はリリース前の信頼確認であって、継続的な検証ではありません。

スクレイプ間隔

レジストリエンドポイントは読み取りロックの下でライブ状態を走査するため、スクレイプは安価ですが無料ではありません。推奨する間隔は次のとおりです。

  • /metrics15 秒ごと(一般的な Prometheus のデフォルト)。集計値のみで、オーバーヘッドは無視できます。
  • /__ox_shared/summary — ダッシュボード向けに 60 秒ごと/metrics よりやや重いです。
  • /__ox_shared/entriesオンデマンドでのみ。すべてのシャードを反復するため、毎ティックでスクレイプしないでください。
  • /__ox_shared/entry / /preview / /graph — 調査中に オンデマンドで。

関連