Prometheus 指标
OxPHP 在内部服务器的 GET /metrics 上以文本展示格式暴露 Prometheus 兼容指标。它们涵盖请求吞吐量、响应时间、连接状态、工作进程池健康状况、静态文件缓存、压缩效率以及工作进程模式的性能。
启用指标
设置 INTERNAL_ADDR 以启动内部服务器:
INTERNAL_ADDR=127.0.0.1:9090然后从 Prometheus 或任何兼容的采集器进行抓取:
curl http://localhost:9090/metrics服务器指标
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_uptime_seconds |
gauge | 服务器进程启动以来的秒数 |
oxphp_requests_total |
counter | 主端口上接收到的 HTTP 请求总数 |
请求指标
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_requests_by_method_total |
counter | 按 HTTP 方法统计的请求数。标签:method(GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、CONNECT、QUERY、OTHER) |
oxphp_responses_by_status_total |
counter | 按状态类别统计的响应数。标签:status(1xx、2xx、3xx、4xx、5xx) |
oxphp_request_bytes_total |
counter | 接收到的请求体字节总数 |
oxphp_response_bytes_total |
counter | 发送的响应体字节总数 |
oxphp_request_cancelled_total |
counter | 按原因统计的已取消请求数。标签:reason(client_abort、timeout、shutdown)。服务器分发的每个请求(而不只是到达 PHP 的那些)都计一次,计数发生在该请求被应答的时刻。因此 client_abort 既包括请求仍在排队时就断开的客户端(常见情形),也包括处理器运行期间断开的客户端;响应已经开始发出之后才断开的客户端已经过了那个时刻,不会被计入。始终输出 |
只有至少记录过一次事件的方法和状态类别才会被输出。计数为零的标签会被省略。
请求耗时直方图
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_request_duration_us |
histogram | 所有请求(静态文件和 PHP)的端到端请求耗时,单位为微秒 |
分桶边界(微秒):100、500、1000、2500、5000、10000、25000、50000、100000、250000、500000、1000000、+Inf。
使用该直方图可以跟踪整体延迟、识别慢端点,并测量尾部延迟百分位数。
连接指标
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_active_connections |
gauge | 主端口上当前打开的 TCP 连接数 |
oxphp_accept_stalled |
gauge | 当接受循环停下来等待一个空闲的 MAX_CONNECTIONS 许可时为 1。此时内核仍在接受连接,但没有任何新请求得到服务,所以从客户端的角度看,服务器已经不再应答。直接对 oxphp_accept_stalled == 1 告警即可,不需要与配置的预算作比较 |
oxphp_accept_stalls_total |
counter | 被接受之后不得不等待 MAX_CONNECTIONS 许可的连接数。即使一次停滞在两次抓取之间开始又结束,它也会变化,而单靠 gauge 会漏掉这种情况:rate(oxphp_accept_stalls_total[5m]) > 0 意味着预算正在被耗尽,哪怕只是短暂的。调高 MAX_CONNECTIONS,或缩小 PHP 积压(QUEUE_CAPACITY + QUEUE_MAX_WAITING);参见为接受循环保留余量 |
oxphp_pending_requests |
gauge | 已接受但尚未应答的 PHP 请求——正在等待队列空位、排队中或执行中。只统计被路由到 PHP 的请求:静态文件、404 或被拒绝的路径不经过队列应答,永远不会出现在这里 |
oxphp_dropped_requests_total |
counter | PHP 工作进程在接受请求后处理失败的请求数 |
oxphp_admission_refused_total |
counter | 未到达工作进程即被应答的请求数。标签:reason,取值有:wait_timeout(等满了整个等待预算,即 QUEUE_WAIT_TIMEOUT_MS,或者在 oxphp_admission_wait_budget_us 显示服务器已将其缩短时更短。请给进程池更多余量,但先看 oxphp_admission_wait_wasted_total,因为如果进程池在这些等待期间什么都没开始处理,补救方法恰恰相反。预算耗尽时客户端已经离开的请求不计入这里:它被应答 499 并计入 oxphp_request_cancelled_total{reason="client_abort"},这样这条序列始终只反映进程池的状况)、waiting_full(已经有 QUEUE_MAX_WAITING 个请求在等待,调高它或 MAX_CONNECTIONS)、waiting_bytes(已停放的请求体填满了 QUEUE_MAX_WAITING_BYTES,这个请求的请求体没有地方可放;调高它,或降低客户端允许上传的大小)、queue_full(QUEUE_WAIT_TIMEOUT_MS=0,等待已关闭)、shutting_down(请求还在等待准入时排空截止时间已过)、pool_unavailable(没有工作线程可以接手请求:进程池已经不在,而不是在忙)。只有前四种属于过载并以 529 应答;shutting_down 与优雅排空的其余部分一样应答 503,pool_unavailable 应答 500。过载告警请专门针对那四种:整个指标在重启时也会变化。不计入 oxphp_queue_wait_us |
oxphp_admission_wait_wasted_total |
counter | 等满了整个等待预算、而进程池在此期间一个请求都没有开始处理的请求数。它是 oxphp_admission_refused_total{reason="wait_timeout"} 的子集,把输给了其他争抢工作进程的请求的等待,与进程池什么都没启动的等待区分开来。客户端已经离开的请求同上不计入。仅在 oxphp_admission_wait_budget_us 处于上限时计数。持续非零意味着等待毫无收益:缩短 QUEUE_WAIT_TIMEOUT_MS。参见白费的等待 |
oxphp_queue_depth |
gauge | 此刻停在工作进程队列中的请求:已被准入,尚未被工作进程取走。条目只有在被取走时才会离开队列,因此这里既统计仍在等待的请求,也统计已经按截止时间被应答的请求(529,或客户端已离开时的 499);深度高于 oxphp_pending_requests 的部分就是这个差值。仅对 SAPI 执行器存在,只有它有队列 |
oxphp_queue_capacity |
gauge | 队列槽位总数:即 QUEUE_CAPACITY,解读 oxphp_queue_depth 时所对照的上限 |
oxphp_admission_slots_available |
gauge | 无人持有的准入许可数。请求进入队列前领取一个,被工作进程取走时归还,所以这就是新到的请求无需等待即可领取的空闲容量。优雅排空期间闸门关闭,无论此值是多少都不会再准入任何请求 |
oxphp_admission_wait_budget_us |
gauge | 此刻到达的请求最多可以等待工作进程多久,单位为微秒(即 oxphp_queue_wait_us 的单位,它为后者设定上界)。QUEUE_WAIT_TIMEOUT_MS 是该值的上限:当等待所准入的工作是在为已经离开的客户端完成时,服务器会将其减半;一旦队列排空、且进程池开始处理的请求中流向已离开客户端的不到四分之一,再逐步调回。低于上限意味着进程池收到的请求超过了它能服务的量,而发出这些请求的客户端比上限更没耐心。在一次事件期间停在下限(上限的六十四分之一,低于 10 ms 时提高到 10 ms,除非上限本身更短)是正常的;在 oxphp_queue_depth 为零时仍停在那里则不正常。快速失败模式(QUEUE_WAIT_TIMEOUT_MS=0)下为 0。没有队列的执行器不输出此项。参见自适应等待预算 |
oxphp_admission_wait_budget_ceiling_us |
gauge | 以微秒表示的 QUEUE_WAIT_TIMEOUT_MS:上面那个 gauge 允许达到的最大值,在进程生命周期内保持不变。有了它,告警只凭这个端点就能把预算与其上限作比较,而不必对照一个写死的数字,与 oxphp_queue_depth 搭配 oxphp_queue_capacity 是同样的配对方式。快速失败模式下为 0。没有队列的执行器不输出此项 |
oxphp_abandoned_work_total |
counter | 工作进程从队列中取出、但客户端已经离开的请求数。上面的等待预算就是根据这个计数来调整的。它在预算下降的同时上升,说明控制器在正常工作;而在预算仍处于上限时上升,则意味着客户端是在处理器运行期间而不是等待期间离开的,因此要缩短的不是等待。它统计的是请求数,而不是浪费的 CPU。按截止时间被拒绝的请求不计入(它们从未运行),响应在处理器结束前就已发出的请求也不计入(已发送响应头的流,或来自 oxphp_finish_request() 的提前应答),所以每个 SSE 会话的结束都不会被计数。这里只能看到关闭了连接的客户端 |
oxphp_pool_stalled |
gauge | 当请求正在等待空闲却毫无进展的工作进程时为 1,即进程池已不再从队列中取走工作。该状态持续一分钟后才会置位(足够长,使得工作进程在回收后重新加载应用不会触发它);当某个工作进程完成一个请求,或进程池在一整分钟内都不再像是卡死时清除。直接对 oxphp_pool_stalled == 1 告警;就绪探针在同一状态下应答 503。仅在工作进程模式下输出;在不监视该状态的地方它不存在,而不是为 0。参见进程池卡死信号 |
对照准入槽位解读队列深度
这两个 gauge 回答的是同一个问题的两半,这个问题是有没有东西在流动,而不是队列有多深。oxphp_queue_depth 说明有什么在等待;oxphp_admission_slots_available 说明还能不能再放进来更多。两者都要对照 oxphp_busy_workers 来读。
| 可用槽位 | 队列深度 | 解读 |
|---|---|---|
> 0 |
0 |
没有东西在等待;正常 |
> 0 |
> 0 且在两次抓取之间下降 |
普通积压:进程池落后了,但正在消化 |
> 0 |
> 0 且不下降,同时 oxphp_busy_workers 为 0 |
进程池已不再从队列中取走工作,而准入仍有余量。这是卡死的第一阶段。到达的请求被准入,各自的预算耗尽时被应答 529(若客户端先放弃则为 499);在预算处于上限时,每个 529 都会让 oxphp_admission_wait_wasted_total 上升。除此之外没有任何迹象:没有工作进程在忙,而单看这些拒绝又像是过载 |
0 |
等于 oxphp_queue_capacity |
队列填满之后的同一种卡死,或者是进程池远远落后的普通过载。oxphp_busy_workers 在多次抓取中持续为 0 可以区分二者;单次采样在过载下也可能读到这样的值 |
0 |
等于或接近 0 |
许可被队列之外的地方持有。单次抓取中,这可能是处于准入与分发之间的请求;在多次抓取中持续如此,则是许可被领走却从未归还:什么都无法被准入,服务器会拒绝每一个 PHP 请求,直到它被重启 |
最后三行是停滞的进程池会呈现的样子。在每一种情形中,其余的指标看上去都很健康,却没有任何 PHP 请求完成:没有忙碌的工作进程,存活探针始终返回 200,就绪探针在第一分钟内也返回 200,静态文件照常提供。拒绝计数器会变化,但它们在普通过载下也会变化。oxphp_pending_requests 随着卡住的队列上升,随后又在每个到达的请求按自己的预算被应答时回落,所以单看它说明不了什么;但对照 oxphp_queue_depth 就能说明问题:pending 接近零而深度停在容量上限,正是卡死的进程池攥着它收到过的所有请求。
卡死恰恰是会让客户端离开的状况。如果前面的负载均衡器的读取超时比 QUEUE_WAIT_TIMEOUT_MS 短,到达的请求就会变成 499,两条拒绝序列都不会变化,因此 oxphp_admission_wait_wasted_total 在整个事件期间都可能毫无动静。这些请求会落入 oxphp_request_cancelled_total{reason="client_abort"},而 oxphp_queue_depth 无论哪种情况都会保持不变。
服务器自己也会注意到卡死。每当连续两次每秒一次的扫描都看到有工作在等待(队列中有请求,或拒绝数在上升),同时至少有一个工作进程空闲、而进程池什么都没完成时,它会记录:
{"timestamp":"2026-08-28T10:56:59.015691Z","level":"WARN","fields":{"message":"PHP requests are waiting while the pool has idle workers and got nothing done since the last scan","queue_depth":22,"queue_capacity":512,"admission_slots_available":490,"workers_idle":4}}只要该状态持续,它大约每分钟重复一次;工作恢复流动后,会接着输出 INFO PHP pool is reaching workers again。这些字段位于 fields 之下,所以告警规则要写 .fields.queue_depth,而不是 .queue_depth。有三道防护避免误报:要两次扫描而不是一次;进度按工作进程完成了什么来衡量,而不是按客户端收到了什么,所以一波客户端中止会被读作它本来的样子,即一个忙碌的进程池;在进程池完成第一个请求之前不做任何报告,所以启动时的引导不会被误认为卡死。
最后这道防护只覆盖进程的首次引导。之后被回收的工作进程(因内存上限或 Worker::scheduleExit())会再次引导其应用,并在接到第一个请求之前被读作空闲,所以在只有一个工作进程的池上(或者其他工作进程都在忙的更大的池上),一次缓慢的引导会产生一条误报警告,随后是它的恢复通知。同一时刻 oxphp_workers_spawned_total 发生变化即可识别这种情况。
该警告仅在工作进程模式下输出,因为只有那种进程池会统计工作进程完成了什么;在工作进程直接阻塞于队列本身的地方,它描述的状态不会出现。这三个 gauge 在所有模式下都会输出。
白费的等待
oxphp_admission_wait_wasted_total 统计的是开始处理的工作,而不是离开队列的条目,因为工作进程清理那些截止时间已经替它们作出应答的请求,会让队列变化,却没有服务任何人。它以预算处于上限为前提,是因为一旦服务器缩短了预算,“进程池什么都没开始”说明的更多是时间窗口而不是进程池:一个健康的四工作进程池运行四分之一秒的处理器时,每 62 ms 开始一个请求,所以在下限处,大多数等待都结束在两次开始之间。
这一前提的代价取决于预算是否变动:
- 一个什么都不开始处理的进程池永远达不到控制器据以决策的已开始请求数,因此它保持在上限,这个计数器也始终有效。
- 一个自我调用的进程池每当外层请求放弃其内层调用时都会开始一个请求。一旦它自己的调用方比它的处理器更没耐心,预算就会下降(实测在几秒内减半两次),这个计数器在下降之前到达的请求处理完后就会安静下来。此时本页没有任何指标能接手:请从应用自身的出站调用中找出这种模式。如果调用方很有耐心(内部定时任务、超时宽松的 sidecar),就没有请求被放弃,上限保持不变,而正是这个计数器能指出问题。
典型原因是应用通过 HTTP 回调同一台服务器,但这个计数器并不是自我调用检测器:一个所有处理器运行时间都超过预算的进程池,在等待期间同样腾不出任何东西。在工作进程模式下,oxphp_worker_request_duration_us(处理器时间,不含队列等待)可以区分二者:处理器时间低于预算而这个计数器在上升,说明这些等待并不只是排在慢工作后面。不要用 oxphp_request_duration_us 来判断:它包含这些拒绝,每一个都贡献了几乎整个预算。在工作进程模式之外,请使用应用自己的计时。
QUEUE_WAIT_TIMEOUT_MS=0 并不是更短预算的最短形式:它去掉的是等待队列槽位的时间,而队列内部的等待不受限制,所以在一个什么都不取走的进程池上,找到空闲槽位的请求永远得不到应答。把 0 与足够小的 QUEUE_CAPACITY 搭配,使得到了关键时刻队列已经是满的。
请求被削减时日志里写了什么
监督器在同样每秒一次的扫描中监视 oxphp_admission_refused_total 的四种过载原因,并按事件而不是按单个请求报告,因为每次拒绝一行本身就会造成故障。第一次看到其中任何一种变化的扫描会记录:
{"timestamp":"2026-09-16T11:02:41.337104Z","level":"WARN","fields":{"message":"PHP admission has started shedding requests under overload","refused":4812,"refused_queue_full":0,"refused_wait_timeout":4812,"refused_waiting_full":0,"refused_waiting_bytes":0,"queue_depth":896,"queue_capacity":896,"admission_slots_available":0,"wait_budget_us":15625,"wait_budget_ceiling_us":1000000}}refused 是那一次扫描所拒绝的数量,但第一次有队列可看的扫描例外,它会报告此前削减的全部请求(一个仍在加载应用、而到达的请求不断堆积的进程池,确实是一次事件)。深度达到容量且没有空闲槽位,是进程池在工作上落后了;wait_budget_us 远低于 wait_budget_ceiling_us 说明服务器已经缩短了等待;两个数值都给出,是因为单看其中一个,被压到下限的预算与本来就配置得这么低的预算看起来一样。队列相关的数值是扫描时的快照,而 refused 覆盖的是刚过去的这一秒,所以很大的 refused 旁边是空队列,说明这是一次短暂的突发,而不是自相矛盾。
两项计数都按原因拆分为 oxphp_admission_refused_total 的四个过载标签,字段名由被拆分的那项计数构成:refused_queue_full、refused_wait_timeout、refused_waiting_full 和 refused_waiting_bytes 加起来等于 refused,而下文的重复警告和结束行带有 episode_refused_queue_full 及其三个同类字段,加起来等于 episode_refused。原因决定了该调哪个参数(上面该计数器那一行说明了每种原因指向什么),而该计数器只在 INTERNAL_ADDR 上提供。在没有内部监听器的实例上,这些字段以及 oxphp.shed_reason span 属性(开启追踪时)就是仍能读到原因的地方。四个字段即使为零也会写出:明确的 0 才能排除某个原因。
事件持续期间,警告会以 PHP admission is still shedding requests under overload 的形式重复,每分钟最多一次,而且只在本身正在拒绝的扫描上输出(所以间隔可能接近两分钟),并在每次扫描的 refused 旁边附带 episode_refused(累计总数)。重复警告按原因拆分的是整个事件的累计数,而不是本次扫描的。这一点很重要,因为结束行以 INFO 级别写出,低于生产配置所使用的 warn 日志级别,所以在那些配置下,重复警告是唯一一行说明该事件到目前为止(截至写出它的那次扫描)由哪些拒绝构成的日志。在第一分钟内就结束的事件根本没有重复警告,而最后一次重复警告之后的拒绝只会出现在结束行中。一整分钟没有拒绝则事件结束,以 INFO 级别记录:
{"timestamp":"2026-09-16T11:05:52.812440Z","level":"INFO","fields":{"message":"PHP admission has stopped shedding requests","episode_refused":18374,"episode_refused_queue_full":0,"episode_refused_wait_timeout":17950,"episode_refused_waiting_full":424,"episode_refused_waiting_bytes":0,"duration_ms":131000,"queue_depth":0,"admission_slots_available":896}}duration_ms 计算到最后一次拒绝为止,而不是到这一行为止,所以结尾那一分钟的安静不计入。这一分钟的安静也意味着短于一分钟的间隙会被并入同一次事件,从而把输出限制在最坏情况下每分钟两行;进入事件则没有这样的延迟,因为拒绝是事实而不是采样。
该报告在所有路由模式下都会输出,并且只随四种过载原因变化,所以一次重启(shutting_down)或一个消失的进程池(pool_unavailable)不会把自己报告成负载问题。这个状态没有对应的 gauge,也不会告知任何探针:削减永远不会改变 /health/readiness 的应答,因为一个正在削减 529 的实例是在一个正常工作的进程池上快速应答,而把每个过载的副本轮流移出轮换,会让 Service 没有任何端点。应对过载的办法是增加容量。卡死的进程池也会削减请求,而在工作进程模式下,该状态持续一分钟后确实会应答 503。队列可以区分二者:只是落后的进程池会把队列排空,卡死的则不会。
工作进程池指标
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_workers_current |
gauge | 当前 PHP 工作进程线程数 |
oxphp_workers_min |
gauge | 最小工作进程数(在静态模式下等于当前数量) |
oxphp_workers_max |
gauge | 最大工作进程数(在静态模式下等于当前数量) |
oxphp_workers_idle |
gauge | 没有任何在途请求的工作线程数,按 workers_current - busy_workers 计算 |
oxphp_busy_workers |
gauge | 当前至少在执行一个请求的工作线程数;绝不会超过 oxphp_workers_current。统计的是线程而不是请求——在工作进程模式下,一个线程多路复用许多请求纤程,也只计一次。等待准入或停在队列中的请求不计入;它们出现在 oxphp_pending_requests 中 |
oxphp_workers_spawned_total |
counter | 自启动以来创建的工作进程总数(含初始工作进程) |
oxphp_workers_retired_total |
counter | 因空闲超时而退役的工作进程总数(仅限动态模式) |
工作进程监督器指标
由工作进程监督器输出的按工作进程粒度的可观测性数据。每条时间序列都带有 worker_id 标签(槽位索引)。一旦监督器开始跟踪按工作进程的状态,这些指标便会出现。
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_worker_request_age_seconds |
gauge | 每个工作进程上处理中请求的存活时长,单位为秒。标签:worker_id |
oxphp_worker_long_running_total |
counter | 监督器扫描时观察到请求存活时间超过卡住阈值(60 秒,固定)的次数。标签:worker_id |
oxphp_worker_stuck_total |
counter | 每个工作进程的卡住分类计数器。标签:worker_id、kind(io、c_call、cpu)。仅用于报告:被判定为卡住的请求不会被取消,其工作进程也不会被回收。服务器中没有任何机制会据此采取行动,所以请对这条序列告警 |
队列等待直方图
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_queue_wait_us |
histogram | 请求在被工作进程取走之前于队列中等待的时长,单位为微秒 |
分桶边界(微秒):50、100、250、500、1000、2500、5000、10000、50000、100000、250000、500000、1000000、+Inf。
它测量的是等待所花的时间,先等准入,再在队列中等待,直到工作进程把请求从队列中取走的那一刻为止,因此回答的是“过了多久才有工作进程接手”,而不是“这个请求总共花了多久”。那一刻之后的一切都不在其中:工作进程准备请求、脚本执行(对于流式响应,到响应头为止),以及把响应交回服务器 I/O 线程的过程,都计入 oxphp_request_duration_us。这次交接并非没有代价:响应要等某个 I/O 线程被唤醒,或等它处理完排在前面的工作,所以对于开销很小的脚本,这次等待之后的时间可能是脚本自身耗时的好几倍。
只有在预算内被工作进程从队列中取走的请求才会记录在这里。因过载被拒绝的 529,或服务器正在关闭、进程池已不在时的 503/500,计入 oxphp_admission_refused_total;工作进程接手之前客户端就已离开的 499 计入 oxphp_request_cancelled_total{reason="client_abort"},不计入任何拒绝序列。
较高的队列等待意味着请求花了很久才到达工作进程,但这本身并不说明工作进程不足:每个读数都包含工作线程接手交给它的请求所需的时间,而这段时间在没有负载的服务器上也存在。在调高 PHP_WORKERS 之前,先确认请求确实发现每个工作进程都在忙,并且机器还有空闲的核心:服务器自己的 I/O 线程、其他进程以及容器的 CPU 配额都在与工作进程竞争。oxphp_busy_workers 对照 oxphp_workers_current 只能部分回答第一个问题:它是在抓取时读取的,所以在两次抓取之间填满进程池的突发会出现在这里而不会出现在那里;而且在工作进程模式下,线程只要承载着任何请求就算忙碌,即使该请求的处理器已挂起、线程正在接更多请求。量程达到一秒,与默认的 QUEUE_WAIT_TIMEOUT_MS 相当,因此一个在运行前花掉了大部分等待预算的请求会被量化出来,而不是被并入 +Inf。任何被服务的请求等待都不会超过预算(超过预算的请求会被拒绝),所以调高 QUEUE_WAIT_TIMEOUT_MS 是唯一会让等待重新落入 +Inf 的设置。
限流指标
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_rate_limited_total |
counter | 被限流器拒绝的请求数(返回 429) |
oxphp_php_deny_total |
counter | 被 PHP_DENY_PATHS 拦截的请求数(.php 执行被拒绝)。参见 PHP 执行拒绝列表 |
oxphp_path_deny_total |
counter | 被 .oxphpdeny 规则拒绝的请求数。同时也被 PHP_DENY_PATHS 拒绝的请求只计入这里。参见路径拒绝规则 |
静态文件缓存指标
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_static_cache_hits_total |
counter | 由内存缓存提供服务的静态文件请求数 |
oxphp_static_cache_misses_total |
counter | 需要从磁盘读取的静态文件请求数 |
压缩指标
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_compressed_responses_total |
counter | 以内容编码(Brotli、zstd 或 gzip)发送的响应数 |
oxphp_compression_bytes_saved_total |
counter | 压缩节省的字节总数(原始大小减去压缩后大小) |
工作进程模式指标
这些指标仅在工作进程模式激活时(WORKER_MODE_ENABLED=true)才会输出。
全局计数器
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_worker_mode_enabled |
gauge | 工作进程模式激活时始终为 1 |
oxphp_worker_requests_handled_total |
counter | 常驻工作进程处理的请求总数 |
oxphp_worker_recycles_total |
counter | 工作进程回收总数:工作进程的循环因下列原因之一而结束。动态池让空闲工作进程退役属于缩容而不是回收:它计入 oxphp_workers_retired_total,而不计入这里或下面的序列 |
oxphp_worker_recycles_by_reason_total |
counter | 按原因统计的回收次数。标签:reason(scheduled、max_memory、error) |
oxphp_worker_soft_resets_total |
counter | 在两次请求之间执行的软重置总数 |
按工作进程的 gauge
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_worker_memory_bytes |
gauge | 每个工作进程在其上一个请求结束时持有的 PHP 堆。这就是处理器内部 memory_get_usage() 返回的值,也是 WORKER_MAX_MEMORY_MIB 所衡量的量。它在上限检查之前稍早读取,此时刚结束请求的超全局变量仍在堆上,所以它比触发回收的数值高出几 KB:请在上限之下告警,而不是恰好在上限处。从未服务过请求的工作进程读数为零。标签:worker(槽位索引,例如 "0"、"1";被回收的工作进程会复用前任的槽位及其最后的值,直到它自己完成一个请求) |
oxphp_worker_uptime_seconds |
gauge | 每个工作进程被创建以来的秒数。标签:worker |
oxphp_worker_requests_count |
gauge | 每个工作进程实例处理的请求数。标签:worker |
oxphp_worker_request_fibers_active |
gauge | 工作进程此刻承载的请求纤程数:空闲时为 0,从头到尾服务一个请求时为 1,请求会挂起并在线程上多路复用时则更高。它在工作进程循环的每一轮都会刷新,而不是在请求完成时,所以在已不再完成请求的工作进程上,它不会冻结在最后一个健康值。一个工作进程最多接纳 256 个请求纤程,停在 256 的工作进程已不再从队列中取走工作。请与 oxphp_busy_workers 一起看:进程池视为空闲的工作进程上有 256 个纤程,说明这些纤程比它们(已经应答的)请求活得更久,且从未被回收;而忙碌工作进程上的 256 则是那么多请求正在处理中。标签:worker |
工作进程请求耗时直方图
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_worker_request_duration_us |
histogram | 每个请求的 PHP 处理器执行时间,单位为微秒(仅限工作进程模式) |
分桶边界(微秒):100、250、500、1000、2500、5000、10000、25000、50000、+Inf。
该直方图测量的是在 PHP 处理器回调内部花费的时间,不包含队列等待时间。可用它来识别慢处理器,并在工作进程模式下跟踪尾部延迟。
异步池指标
这些指标需要将 ASYNC_WORKERS 设为非零值,并且每项都有各自的输出门槛:计数器仅在至少有一个任务被分发或被拒绝后才会出现,_in_flight / _in_flight_limit gauge 在池连接好其处理中计数器后出现,而 oxphp_async_output_discarded_bytes_total 仅在有部分输出被丢弃后才会出现。
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_async_tasks_dispatched_total |
counter | 分发给后台池的异步任务总数 |
oxphp_async_tasks_completed_total |
counter | 成功完成的异步任务数 |
oxphp_async_tasks_failed_total |
counter | 抛出异常的异步任务数 |
oxphp_async_tasks_cancelled_total |
counter | 被取消的异步任务数 |
oxphp_async_tasks_rejected_total |
counter | 在分发时被拒绝的异步任务数——因为池队列已满或达到处理中上限(ASYNC_MAX_FIBERS × ASYNC_WORKERS) |
oxphp_async_tasks_stranded_total |
counter | 超过 await_race / await_any 超时后仍在运行的工作进程。每个滞留任务最多可将 RSHUTDOWN 延长 5 秒。 |
oxphp_async_tasks_in_flight |
gauge | 当前排队或运行中的异步任务(在池连接好其处理中计数器后输出) |
oxphp_async_tasks_in_flight_limit |
gauge | 并发异步任务的最大数量(ASYNC_MAX_FIBERS × ASYNC_WORKERS) |
oxphp_async_output_discarded_bytes_total |
counter | 在工作进程空闲时丢弃的异步任务输出字节数(异步任务中的 echo 没有客户端可接收) |
OpenTelemetry 导出指标
这些指标需要 OTEL_ENABLED=true。它们统计 OxPHP 发送给 OTLP collector 的 span 批次:请求 span,以及启用 APM 插件时的 APM span。
| 指标 | 类型 | 说明 |
|---|---|---|
oxphp_otel_export_failures_total |
counter | 失败的 span 导出数(进行过重试的,按重试之后的结果计)。其中的 span 不会再次发送 |
oxphp_otel_export_failed_spans_total |
counter | 这些失败的导出所携带的 span 数 |
oxphp_otel_export_retries_total |
counter | 因第一次尝试快速失败、没有得到 collector 应答而重试一次的导出数 |
当导出的第一次尝试在 OTEL_EXPORTER_OTLP_TIMEOUT 的一半或一秒(取较短者)之内失败、且没有得到 collector 的应答时,它会被重试一次:连接被重置或关闭,或者无法建立。常见原因是负载均衡器或 NAT 在两次导出之间连接空闲时把它丢弃了。collector 应答的错误状态(例如 API 密钥被拒绝或限流)不会重试:OTLP 规范禁止重试其中的大多数,而且导出器也不会转达限流中的 collector 所要求的延迟。成功送达批次的重试只计入 oxphp_otel_export_retries_total。撞上导出超时的尝试不会重试,因此慢速的 collector 永远不会收到同一批次两次。
不过,重试可能造成重复。如果 collector 在连接断开之前确实收到了第一次尝试,重试会把那些 span 再送达一次;出于同样的原因,被计为失败的导出也可能已经到达了 collector。
追踪器因队列已满(OTEL_BSP_MAX_QUEUE_SIZE,默认 2048 个 span)而在导出前丢弃的 span 不计入这里。
Grafana 仪表盘技巧
以下 PromQL 查询有助于构建仪表盘:
请求速率(每秒请求数):
rate(oxphp_requests_total[5m])平均响应时间(毫秒):
rate(oxphp_request_duration_us_sum[5m])
/ rate(oxphp_requests_total[5m]) / 1000p99 请求耗时(毫秒):
histogram_quantile(0.99, rate(oxphp_request_duration_us_bucket[5m])) / 1000错误率(5xx 响应的百分比):
rate(oxphp_responses_by_status_total{status="5xx"}[5m])
/ rate(oxphp_requests_total[5m]) * 100工作进程池利用率:
oxphp_busy_workers / oxphp_workers_current这是一个介于 0 和 1 之间的真分数。持续处于 1 意味着每个工作线程都被占用,继续到来的请求正在排队。把它与 rate(oxphp_admission_refused_total{reason=~"queue_full|wait_timeout|waiting_full|waiting_bytes"}[5m]) 搭配,看积压是否正在变成拒绝;再与 oxphp_pending_requests 搭配,看积压有多深。
队列饱和度(每秒丢弃率):
rate(oxphp_dropped_requests_total[5m])p99 队列等待时间(微秒):
histogram_quantile(0.99, rate(oxphp_queue_wait_us_bucket[5m]))静态文件缓存命中率:
rate(oxphp_static_cache_hits_total[5m])
/ (rate(oxphp_static_cache_hits_total[5m]) + rate(oxphp_static_cache_misses_total[5m]))每秒因压缩节省的字节数:
rate(oxphp_compression_bytes_saved_total[5m])工作进程模式 p99 延迟(微秒):
histogram_quantile(0.99, rate(oxphp_worker_request_duration_us_bucket[5m]))等待预算被压到上限以下(比例):
oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us1 是正常的;任何更低的值都意味着服务器正在过载下缩短等待。请对这个比值告警,而不是对从 QUEUE_WAIT_TIMEOUT_MS 抄来的数字告警。
接受循环停滞(非零时告警):
rate(oxphp_accept_stalls_total[5m])工作进程回收速率(每分钟):
rate(oxphp_worker_recycles_total[5m]) * 60工作进程平均内存使用量:
avg(oxphp_worker_memory_bytes)Prometheus 抓取配置
在你的 prometheus.yml 中添加一个抓取任务:
scrape_configs:
- job_name: "oxphp"
scrape_interval: 15s
static_configs:
- targets: ["oxphp:9090"]对于 Kubernetes 服务发现:
scrape_configs:
- job_name: "oxphp"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: oxphp
action: keep
- source_labels: [__meta_kubernetes_pod_ip]
target_label: __address__
replacement: "$1:9090"给你的智能体的提示词
这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。
让 Prometheus 抓取我的 OxPHP 服务。通过 OxPHP 文档 MCP 服务器读取 `operations/metrics` 页面。
用这个仓库里已有的服务名,加上抓取配置,以及我的 Compose 或 Kubernetes 文件里为了让指标端点可达所需要的一切。帮我做一个 OxPHP 的 Grafana 入门仪表盘。通过 OxPHP 文档 MCP 服务器读取 `operations/metrics` 页面。
挑出那些真正能说明我的应用是否健康的指标,并解释每块面板在数值变动时告诉了我什么。