Shared* 可观测性

每个 OxPHP\Shared\* 实例都是运行时为引用计数和容量而追踪的一个注册表条目。这份追踪信息以 /__ox_shared/* 下的 JSON 自省接口,以及 oxphp_shared_* 下的 Prometheus 指标形式暴露给运维人员。本页既是参考手册,也是现场排查指南。

启用

可观测性搭载在内部服务器之上。设置 INTERNAL_ADDR 即可启动它:

bash
INTERNAL_ADDR=127.0.0.1:9090

JSON 端点和 /metrics 随后都可在该地址访问。无需任何额外配置。

你可以分别独立地禁用其中任意一项:

Env var Default Effect
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。单次抓取即可获得每种类型的健康状况和容量余量。

GET /__ox_shared/entries?limit=N

列出实时条目(上限为 limit,默认 100,最大 500)。每个条目占一行:

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

针对单个条目的、按类型区分的详细信息:

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 类型目录,对于需要标签 → 类映射的生成式工具很有用:

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 之后使用 graph,查看遍历器所走过的可达路径;或者在诊断“为什么这个 Counter 没有被 GC 回收”时使用它:图会显示出每一个对它持有保留计数的父节点。

Prometheus 指标

所有指标都在 GET /metrics 处与核心服务器指标一同暴露。

注册表全局

Metric Type Labels Description
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 检测到的跨线程等待环。

Channel

Metric Type Labels
oxphp_shared_channel_count gauge channel_id
oxphp_shared_channel_pending (deprecated) 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

Metric Type Labels
oxphp_shared_map_entries gauge map_id
oxphp_shared_map_max_entries gauge map_id
oxphp_shared_map_saturation gauge map_id

Pool

Metric Type Labels
oxphp_shared_pool_count gauge pool_id
oxphp_shared_pool_size (deprecated) 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_count 被钉在 maxSize

检查:

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

查看 idle_by_thread。如果它是 {} 或严重失衡(worker 0 有 8 个空闲,worker 3 有 0 个),说明获取操作正在争抢那些恰好在别处繁忙的线程。v1 中的每线程亲和性不会做再平衡。要么提高 maxSize,要么降低每线程的获取热点。

如果 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/entries 中的 refcount 会告诉你有多少个未释放的保留计数。如果在 PHP 包装器离开作用域后它仍然大于 1,说明另一个 Shared 条目正让它保持存活。

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

反向遍历这张图。任何能到达这个卡住条目的节点都持有着一个保留计数。移除该引用($map->remove($key)、关闭 channel、丢弃 Mutex 条目),refcount 就会下降。

生产环境触发了 CycleException

该异常的消息包含环检测器所探索的可达路径。通过 /__ox_shared/entries 将这些 ID 映射回类型,并向 /__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. 如果这个环是一个真实的重入 bug,请重构为每个锁定作用域使用独立的 mutex。
  3. 在预发环境中设置 SHARED_LOCK_DIAGNOSTICS=strict,把未来的重入变成快速失败,而不是一个被检测到的环。

长时间浸泡测试框架

tests/soak/pool_soak.sh 是一个手动(非 CI)测试框架,用于在数小时或数天的持续负载下验证 Shared\Pool 的稳定性表现。它会:

  1. 以动态工作进程伸缩(默认 PHP_WORKERS=4:40)和一个较短的池 idleTimeout 启动开发镜像,从而让逐出调度器持续触发。
  2. 加载 tests/soak/workload.php 作为工作进程引导脚本,它会构造 10 个池 × maxSize=8,并在每个请求上提供 acquire/release 服务。
  3. wrk 驱动流量,持续 SOAK_DURATION_MIN 分钟(默认 1440 = 24h)。
  4. 每 60 秒抓取一次 /metrics 和容器的 RSS,写入 tests/soak/out/<timestamp>/metrics.csv
  5. 在结束时写出 verify.txt,标注五项发布退出准则的通过/失败(RSS 漂移在 ±5% 以内、零陈旧句柄 panic、关闭时零泄漏条目、空闲超时逐出平稳上升、零死锁检测器触发)。

主机上的前置要求: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 — 每分钟一行(unix 时间戳、RSS、按类型的条目计数、按池的逐出计数器、死锁计数、操作数)。
  • server.log — 容器 stdout/stderr,包括任何陈旧句柄或 panic 的踪迹。
  • wrk.out / wrk.err — 负载生成器的原始输出。
  • metrics.final — 在容器拆卸前刚刚采集的最后一次 /metrics 抓取。用于确认负载停止后条目数和字节数已回到基线(无泄漏的 Shared 条目)。
  • verify.txt — 五项退出准则的通过/失败报告。
Warning

不要把它接入 CI。一次 24h 的运行代价不菲,其目的是发布前的信心保证,而非持续验证。

抓取节奏

注册表端点会在读锁下遍历实时状态,因此抓取虽便宜但并非免费。推荐的节奏:

  • /metrics每 15 秒(典型的 Prometheus 默认值)。仅为聚合值;开销可忽略。
  • /__ox_shared/summary — 仪表盘用 每 60 秒。比 /metrics 略重。
  • /__ox_shared/entries — 仅按需。它会遍历所有分片;不要每个 tick 都抓取。
  • /__ox_shared/entry / /preview / /graph — 在排查期间按需使用。

相关内容