Shared* オブザーバビリティ
すべての OxPHP\Shared\* インスタンスは、ランタイムが refcount と容量のためにすでに追跡しているレジストリエントリです。その追跡内容は、/__ox_shared/* 配下の JSON イントロスペクションと、oxphp_shared_* 配下の Prometheus メトリクスとして運用者に公開されます。このページはそのリファレンスであり、実践ガイドでもあります。
有効化
オブザーバビリティは内部サーバーに相乗りしています。起動するには INTERNAL_ADDR を設定します。
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
トップレベルのスナップショット。型ごとの集計数、メモリ、オペレーションレート、そして設定された上限に対する飽和度が得られます。
{
"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 行です。
{
"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 つのエントリに対する型ごとの詳細です。
{
"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 によって制御されます。
{ "id": 42, "type": "Counter", "preview": "1420" }preview は開発中に使ってください。値にユーザーデータが含まれる可能性がある本番環境では無効にします。
GET /__ox_shared/types
v1 の型カタログを列挙します。tag → class のマッピングを必要とする自動生成ツールに便利です。
{
"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=16、edges=500 です。ウォーカーの予算に達すると、レスポンスで truncated: true が設定されます。
{
"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 |
上限に対する割合としての entries と bytes。 |
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 |
oxphp_shared_channel_pending は
oxphp_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 |
oxphp_shared_pool_size は
oxphp_shared_pool_count の旧称です。非推奨期間中は両方の系列が
同じ値を持ちますが、将来のリリースでエイリアスが削除されると
乖離します。新しいダッシュボードは _count を対象にしてください。
oxphp_shared_pool_evicted_total のラベル: reason=idle_timeout | evict | shutdown。idle_timeout はアイドルスロットの自動エビクション、evict は明示的な Pool::evict() 呼び出し、shutdown はプロセス終了時のティアダウンです。
Counter / Flag / Once / Mutex
インスタンスごとの Counter、Flag、Once、Mutex は、個別のメトリクス系列を提供しません。ラベルのカーディナリティが肥大化してしまうためです。インスタンスごとの調査には、代わりにレジストリ全体の oxphp_shared_operations_total{type=...} カウンターと /__ox_shared/entry?id=… の JSON を使ってください。
フォローアップ作業として追跡しています。現時点での可視性は /__ox_shared/entry 経由です。
診断プレイブック
Pool が飽和している(リトライが失敗する 429)
症状: HTTP の呼び出し元がタイムアウトを受け取り、oxphp_shared_pool_waiting が上昇し、oxphp_shared_pool_count が maxSize に張り付きます。
確認:
curl -s http://localhost:9090/__ox_shared/entry?id=<pool_id> | jq .type_specificidle_by_thread を確認します。これが {} であるか、大きく偏っている場合(ワーカー 0 に 8 個のアイドル、ワーカー 3 に 0 個)、acquire は、たまたま別の場所でビジーになっているスレッドを奪い合っています。v1 のスレッドごとのアフィニティはリバランスしません。maxSize を引き上げるか、スレッドごとの acquire のホットスポットを減らしてください。
idle_by_thread が均衡しているのにすべてが in_use になっている場合は、maxSize を引き上げてください。
メモリの飽和
oxphp_shared_total_bytes と oxphp_shared_capacity_saturation{kind="bytes"} を確認します。どちらかが高い場合:
curl /__ox_shared/entries?limit=500を実行し、mem_bytesでソートして、上位の要因を見つけます。- それぞれに対して
curl /__ox_shared/entry?id=<N>を実行して形状を確認します。Map の場合はkey_countとmax_entriesを見ます。 - 最も一般的な原因: ユーザー入力をキーとする、上限のない
Shared\Map。対処法はmaxEntriesの上限と保持ポリシーです。
ラッパーがガベージコレクトされない
/__ox_shared/entries の refcount は、未解放の保持がいくつあるかを教えてくれます。PHP ラッパーがスコープを抜けた後も 1 を超えたままの場合、別の Shared エントリがそれを生かし続けています。
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> に問い合わせます。
# 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 と所有スレッドを含むログレコードをサイクルごとに出力します。復旧するには:
- それぞれに対して
curl /__ox_shared/entry?id=<mutex_id>を実行し、poisoned=falseを確認します。ポイズニングされている場合、検出器はすでにそのサイクルを中断しています。 - サイクルが本当の再入バグである場合は、ロックスコープごとに別々の mutex を使うようリファクタリングします。
- ステージングで
SHARED_LOCK_DIAGNOSTICS=strictに引き上げると、今後の再入は、検出されるサイクルではなく即時失敗(fast-fail)になります。
長時間ソークハーネス
tests/soak/pool_soak.sh は、数時間から数日にわたる継続的な負荷をかけて Shared\Pool の安定性を検証するための、手動(非 CI)ハーネスです。このハーネスは次のことを行います。
- 動的なワーカースケーリング(デフォルトで
PHP_WORKERS=4:40)と短いプールidleTimeoutを指定して dev イメージを起動し、エビクションスケジューラーが継続的に発火するようにします。 tests/soak/workload.phpをワーカーのブートストラップとして読み込みます。これは 10 個のプール ×maxSize=8を構築し、リクエストごとに acquire/release を処理します。wrkでSOAK_DURATION_MIN分間(デフォルト 1440 = 24 時間)トラフィックを流します。/metricsとコンテナの RSS を 60 秒ごとにスクレイプしてtests/soak/out/<timestamp>/metrics.csvに記録します。- 最後に、5 つのリリース終了基準(RSS のドリフトが ±5% 以内、stale-handle パニックがゼロ、シャットダウン時のリークエントリがゼロ、idle-timeout エビクションが滑らかに増加、デッドロック検出器の発火がゼロ)に対する合否を
verify.txtに書き出します。
ホスト側の前提条件: docker、wrk、curl、awk。
典型的な実行例:
# 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 つの終了基準に対する合否レポート。
これを CI に組み込まないでください。24 時間の実行は安くはなく、その目的はリリース前の信頼確認であって、継続的な検証ではありません。
スクレイプ間隔
レジストリエンドポイントは読み取りロックの下でライブ状態を走査するため、スクレイプは安価ですが無料ではありません。推奨する間隔は次のとおりです。
/metrics— 15 秒ごと(一般的な Prometheus のデフォルト)。集計値のみで、オーバーヘッドは無視できます。/__ox_shared/summary— ダッシュボード向けに 60 秒ごと。/metricsよりやや重いです。/__ox_shared/entries— オンデマンドでのみ。すべてのシャードを反復するため、毎ティックでスクレイプしないでください。/__ox_shared/entry//preview//graph— 調査中に オンデマンドで。
関連
- 共有状態 — メンタルモデルとプリミティブの概要。
- Prometheus メトリクス — 同じ
/metricsエンドポイント配下のコアサーバーメトリクス。 - 内部サーバー —
/__ox_shared/*エンドポイントがINTERNAL_ADDRにどう接続されるか。 - 外部ストアへの移行 — 飽和がチューニング可能ではなく構造的である場合。