健康检查

OxPHP 在一个独立端口上运行内部 HTTP 服务器,用于健康监控、指标采集和配置检查。它与应用流量相互隔离,因此监控永远不会与用户请求争抢资源。

配置

设置 INTERNAL_ADDR 以启用内部服务器:

bash
INTERNAL_ADDR=127.0.0.1:9090

当未设置 INTERNAL_ADDR 时,内部服务器不会启动,也没有任何健康检查端点可用。

Note

仅指定端口的 INTERNAL_ADDR(:9090)会绑定到 127.0.0.1;如需将内部服务器暴露到宿主机之外,请显式使用 0.0.0.0:9090。当它可以从宿主机之外访问时,请用 INTERNAL_ALLOW_IPS(一个 CIDR/IP 允许列表)来限制访问——/metrics、/config 以及插件路径会对该列表之外的对端返回 403,而健康检查探针仍然可以访问;环回地址并不会隐式包含在内,因此需要列出 127.0.0.1/32 以保留本地访问。如果监听器已暴露却未设置允许列表,服务器会在启动时发出警告。

Kubernetes 探针

OxPHP 为每种 Kubernetes 探针类型都提供了专用端点。每个端点还提供一个简短别名(/healthz、/readyz、/startupz)。

端点 别名 检查内容 200 503
/health/liveness /healthz 无(能响应即视为存活) 始终 永不
/health/readiness /readyz 未在关闭、工作线程在运行、进程池未卡死、无失败插件 就绪 未就绪
/health/startup /startupz 工作线程在运行 就绪 未就绪

存活(Liveness) 始终返回 200 OK。只要进程能够响应 HTTP 请求,它就是存活的。不执行任何执行器或插件检查——这可以防止 Kubernetes 因工作进程池的瞬时问题而重启 pod。

就绪(Readiness) 在以下情况返回 503 Service Unavailable:

  • 服务器正在关闭(优雅关闭进行中)
  • 所有 PHP 工作线程都已结束(进程池已经没有可以接手请求的线程)
  • 进程池卡死:请求在等待工作进程,而工作进程空闲着,却什么也没完成。只有在工作进程模式下才会检测这种状态(参见进程池卡死信号)
  • 任何插件报告了失败

在优雅关闭期间,就绪检查会立即返回 503,从而使 Kubernetes 在排空完成之前就把该 pod 从 Service 端点中移除。

启动(Startup) 在没有任何工作线程运行时返回 503 Service Unavailable。使用此探针可以防止在缓慢初始化期间过早触发存活检查而杀掉进程。它报告的是线程,而线程在进程池创建时就已存在;它不报告在线程中运行的应用自身的引导过程。

进程池卡死信号

一个进程池可能在它拥有的每个线程都活着的情况下,停止从队列中取走工作:请求不断堆积,没有工作进程在忙,排队的请求一个也不会被执行。每个请求在耗尽自己的等待预算后仍会得到应答(529,或者在客户端先放弃等待时是 499),所以从外部看它并不像是挂起了。请求仍有应答,但没有一个被执行,而队列照样会被填满,因为只有被取走才会释放一个条目。在这种状态下,其他所有信号读起来都是健康的:线程都在,插件正常,静态文件照常提供。一个进程池如果一分钟后仍处于这种状态,通常不会自行恢复,这就是就绪检查要报告它的原因。

服务器会监视一种正常服务的进程池不会持续出现的组合:有工作在等待(队列中有请求,或准入拒绝在攀升),同时至少有一个工作进程空闲,并且自上一次扫描以来进程池没有完成任何工作。

  • 连续两次一秒一次的扫描就足以写出一行日志。
  • 就绪检查会再等一分钟。一个所有工作进程同时停止的进程池(最常见的是只有一个工作进程的进程池),在替代进程重新运行应用引导的期间,恰好呈现出这种形态。新的工作进程远在这一分钟结束之前就重新开始服务了,而过早摘除该副本,会拿走它证明自己所需要的流量。
  • 当某个工作进程真正完成了一个请求(一段空闲不算恢复),或者经过了一整分钟、期间进程池完全没有显得卡死时,就绪检查即恢复。第二种出口不需要流量:一个卡死的进程池会一直占着它的队列,因此无法满足这个条件,而被误判为卡死的进程池则可以。没有它,这种状态就会是永久的,因为 503 恰好挡住了那些其完成本是另一条出路的请求。
Note

该信号需要统计工作进程处理完的请求数,而只有工作进程模式(WORKER_MODE_ENABLED=true)会维护这个计数,因此它只存在于工作进程模式中。在其他模式下,就绪检查依据其余三个条件作答,而 oxphp_pool_stalled 不会被导出,而不是作为恒定的 0 导出。

探针不报告的情况

负载。 过载的服务器保持 Ready。一旦准入队列已满、等待预算已耗尽,请求就会以 529 Site is overloaded 被拒绝,而这样做的副本依然在应答,应答得很快,而且进程池运转正常。把它移出轮转会把它的流量转移到相邻副本上,而在均匀过载下,这会依次移除每一个副本,直到 Service 没有任何端点:一个降级的站点变成了无法访问的站点,而负载依然存在。请改为观察 oxphp_admission_refused_total{reason=...} 并增加容量。不过,请把 oxphp_admission_wait_wasted_total 与 oxphp_admission_wait_budget_us 放在一起看:wait_timeout 拒绝本身并不意味着进程池在完成它的工作;如果在请求的整段等待期间进程池都没有空出任何工作进程,补救方法恰好相反:缩短等待预算,而不是扩大进程池。该计数器只在预算处于上限时才计数。服务器也会在日志中说明这一点:在开始卸除请求的那次扫描上输出一条 WARN,在持续期间大约每分钟重复一次,并在整整一分钟没有拒绝之后输出一条 INFO。参见 Prometheus 指标。

连接预算耗尽。 当 MAX_CONNECTIONS 许可用尽时,接受循环会停摆,新客户端得不到任何应答,但内部监听器不经过这一预算,因此探针仍会继续响应。这同样是负载而非故障,不会改变就绪状态;oxphp_accept_stalled 会报告它。

响应缓慢。 这里没有任何东西测量延迟。一个对所有请求都应答迟缓的进程池是 Ready 的。

所有探针端点都返回 Content-Type: text/plain,其响应体为探针名称(例如 readiness)。Kubernetes 只检查 HTTP 状态码。

bash
# Quick check curl -s -o /dev/null -w '%{http_code}' http://localhost:9090/health/readiness

GET /health

以 JSON 形式返回完整的服务器健康状态。请将其用于仪表盘和监控系统,而不是用于 Kubernetes 探针。

bash
curl http://localhost:9090/health

健康响应(200 OK):

json
{ "active_connections": 7, "executor_healthy": true, "plugins": {}, "pool_stalled": false, "status": "ok", "total_requests": 48203, "uptime_secs": 3612 }

降级响应(503 Service Unavailable):

json
{ "active_connections": 7, "executor_healthy": false, "plugins": {}, "pool_stalled": false, "status": "degraded", "total_requests": 48203, "uptime_secs": 3612 }
字段 类型 说明
status string 所有子系统健康时为 "ok",否则为 "degraded"。负载不属于子系统故障:在过载下卸除 529 的服务器报告 "ok"
uptime_secs integer 服务器启动以来的秒数
total_requests integer 主端口上处理的 HTTP 请求总数
active_connections integer 主端口上当前打开的连接数
executor_healthy boolean 是否仍有 PHP 工作线程在运行。false 表示进程池已经没有可以接手请求的线程
pool_stalled boolean 进程池是否卡死:请求在等待工作进程,而工作进程空闲着,却什么也没完成。在工作进程模式之外始终为 false,因为那里不检测这种状态(参见进程池卡死信号)。true 会使 HTTP 状态切换为 503
plugins object<string, string> 各插件的健康状况:键为插件名称,值为 "ok"、"degraded" 或 "failed"。当没有插件报告健康状况时为空的 {}。"failed" 插件会使 HTTP 状态切换为 503;"degraded" 会出现在此处,但状态仍保持为 200。

GET /metrics

以文本展示格式返回与 Prometheus 兼容的指标。完整的指标参考请见 Prometheus 指标。

bash
curl http://localhost:9090/metrics

GET /config

以 JSON 形式返回当前生效的服务器配置。出于安全考虑,TLS 证书与密钥路径、internal_addr 以及 error_pages_dir 会从响应中被清除。

bash
curl -s http://localhost:9090/config | jq .
json
{ "access_log": "all", "async_in_flight_cap": 0, "async_max_fibers": 256, "async_queue_capacity": 0, "async_workers": 0, "brotli_level": 5, "deny_file": {"loaded": false}, "document_root": "/var/www/html/public", "drain_timeout_seconds": 25, "entry_file": "/var/www/html/public/index.php", "executor_type": "sapi", "gzip_level": 6, "header_timeout_seconds": 5, "listen_addr": "0.0.0.0:443", "log_level": "info", "max_connections": 10000, "max_query_body": 524288, "php_workers": "8", "plugins": {}, "queue_capacity": 1024, "queue_max_waiting": 1024, "queue_max_waiting_bytes": 67108864, "queue_wait_timeout_ms": 1000, "rate_limit": 100, "rate_window_seconds": 60, "runtime_hooks": [], "static_max_age": 2592000, "static_revalidate": false, "superglobals_enabled": true, "tls_enabled": true, "tls_min_version": "1.2", "tokio_workers": 4, "trace_context": false, "trusted_proxies": false, "worker_max_memory_mib": 0, "worker_mode_enabled": false, "zstd_level": 6 }
Note

TLS 证书与密钥路径永远不会被输出(tls_enabled 表示 TLS 是否处于启用状态),而 internal_addr 和 error_pages_dir 会从对外提供的响应中被清除。

Kubernetes 集成

为每种探针类型使用专用的探针端点:

yaml
apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: oxphp image: ghcr.io/oxphp/oxphp:0.13.0 env: - name: INTERNAL_ADDR value: "0.0.0.0:9090" - name: INTERNAL_ALLOW_IPS # the Prometheus scraper's network, plus loopback value: "10.0.0.0/8,127.0.0.1/32" ports: - containerPort: 80 - containerPort: 9090 startupProbe: httpGet: path: /health/startup port: 9090 initialDelaySeconds: 1 periodSeconds: 2 failureThreshold: 15 livenessProbe: httpGet: path: /health/liveness port: 9090 periodSeconds: 10 failureThreshold: 3 readinessProbe: httpGet: path: /health/readiness port: 9090 periodSeconds: 5 failureThreshold: 2
探针 失败时的影响
启动 Kubernetes 等待——在初始化期间不会杀掉 pod
存活 Kubernetes 重启 pod
就绪 Kubernetes 将 pod 从 Service 端点中移除(不重启)

简短别名(/healthz、/readyz、/startupz)与完整路径完全等价,可以用来替代它们。

Docker Compose 健康检查

本示例把内部服务器绑定到 127.0.0.1,而 Kubernetes 示例绑定的是 0.0.0.0:Docker 在容器自身的网络命名空间内运行 healthcheck 命令,在那里环回地址可以访问到监听器;而 kubelet 是从 pod 网络命名空间之外发送其 httpGet 探针的,在那里环回地址访问不到监听器。

compose.yaml
services: oxphp: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "8080:80" environment: INTERNAL_ADDR: "127.0.0.1:9090" healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9090/health"] interval: 10s timeout: 5s retries: 3 start_period: 5s

在配置的重试次数全部失败后,Docker 会将容器标记为 unhealthy,这可能触发重启策略或将其从负载均衡器中移除。

给你的智能体的提示词

这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。

prompt
帮我的项目配好 OxPHP 健康检查。通过 OxPHP 文档 MCP 服务器读取 `operations/health-checks` 页面。 根据这个仓库已经定义好的服务和端口,在我的 docker-compose.yml 里加上健康检查,并给我的 Kubernetes Deployment 加上存活探针和就绪探针。
prompt
我的 OxPHP 容器一直被编排系统重启,我不知道为什么。通过 OxPHP 文档 MCP 服务器读取 `operations/health-checks` 页面。 把我的健康检查配置和内部服务器实际暴露的东西对照一下,告诉我错的是这两者中的哪一个。

另见

发现错误?报告问题 →