内部服务器

OxPHP 会在一个专用端口上运行一个独立的 HTTP 服务器,用于健康检查、Prometheus 指标以及实时配置查看。它与主应用监听器完全隔离:没有 TLS,没有限流,没有请求 ID,也不做事件处理。

工作原理

将 INTERNAL_ADDR 设置为一个监听地址(例如 127.0.0.1:9090),OxPHP 就会在该地址上启动第二个 HTTP 监听器。内部服务器会暴露下列内置端点:

  • /health — 聚合的 JSON 状态
  • /health/liveness、/healthz(别名)— 存活探针
  • /health/readiness、/readyz(别名)— 就绪探针
  • /health/startup、/startupz(别名)— 启动探针
  • /metrics — Prometheus 格式输出
  • /config — 当前生效的服务器配置 JSON

插件可以在 /__<plugin>/ 前缀下注册额外的端点。在优雅关闭期间,内部服务器会一直保持可用,直到主服务器完成连接排空。

Note

只有在显式设置了 INTERNAL_ADDR 时,内部服务器才会启动。若不设置,健康检查、指标和配置端点将无法通过 HTTP 访问。

配置

Variable Default Description
INTERNAL_ADDR (unset) 内部服务器的地址。未设置时不启动。仅指定端口的值(:9090)会绑定到 127.0.0.1;要将其暴露到主机之外,请使用显式的 0.0.0.0:9090。示例:127.0.0.1:9090
INTERNAL_ALLOW_IPS (unset) 以逗号分隔的 CIDR/IP 允许列表。列表之外的对端在访问 /metrics、/config 以及 /__<plugin>/ 路径时会收到 403;健康探针始终保持可达。未设置/为空 = 允许所有。回环地址不会被隐式包含——请列出 127.0.0.1/32 以保留 localhost 访问。列表格式错误会中止启动

端点

GET /health

返回 JSON 格式的健康状态,供仪表盘和容器健康检查使用。对于 Kubernetes,请改用专门的探针端点(参见 Kubernetes 集成):/health 会一次性汇报所有子系统的状态,因此若把它接到存活探针上,其中任何一个出现故障都会导致 Pod 被重启。

200 OK — 所有系统健康:

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

503 Service Unavailable — 某个插件报告了失败:

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

plugins 对象会列出每个已加载的插件及其健康状态:"ok"、"degraded" 或 "failed"。当任意插件报告 "failed"、没有任何 PHP 工作线程仍在运行(executor_healthy: false),或者工作池陷入停滞(pool_stalled: true,仅限工作进程模式;参见 Health Checks)时,该端点会返回 503。"degraded" 的插件会出现在 JSON 响应体中,但 HTTP 状态码仍为 200。负载同样不算故障:在过载时以 529 拒绝请求的服务器仍会报告 "ok"。

GET /metrics

以文本格式(text/plain; version=0.0.4; charset=utf-8)返回 Prometheus 兼容的指标。始终返回 200。

bash
curl http://localhost:9090/metrics
text
# HELP oxphp_requests_total Total HTTP requests # TYPE oxphp_requests_total counter oxphp_requests_total 48203 # HELP oxphp_active_connections Current open connections # TYPE oxphp_active_connections gauge oxphp_active_connections 7 ...

有关可用指标的完整列表,请参阅 Metrics。

GET /config

以 JSON 形式返回当前生效的服务器配置。始终返回 200。

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": { "allow": 1, "deny": 3, "entry": 1, "fallback": "404", "loaded": true, "rules": 5 }, "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": ["sleep", "streams", "net"], "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 }

runtime_hooks 列出进程在启动时安装的运行时钩子类别。它是解析后的集合,而不是原始的 RUNTIME_HOOKS 值,因此 all、1 和 sleep, streams, net 都会显示为 ["sleep", "streams", "net"],抓取程序无需重新实现这套语法。空列表表示没有安装任何钩子,变量名拼写错误时也会得到这个结果。它报告的是已安装的内容,而不是实际生效的内容:在完全没有 fiber 的场景下,钩子会委托给被它们替换的内置函数,因此传统模式下的服务器会列出它所设置的类别,但行为与原生无异。参见 运行时钩子。

deny_file 汇总了启动时从 DOCUMENT_ROOT 读取的 .oxphpdeny 文件:按动作拆分的规则数量(deny;entry 对应 > 规则;allow 对应 ! 规则),以及 deny 规则的响应方式,即 PHP_DENY_FALLBACK 状态码或 "script"(不含脚本路径)。规则本身不会被输出:在可从外部访问的内部服务器上,它们恰好构成一张该文件所保护路径的地图。要查看每条规则是如何被解析的,请以 LOG_LEVEL=debug 启动服务器,并查找 oxphpdeny rule 日志行。如果没有该文件,此条目为 {"loaded": false}。

TLS 证书和密钥文件路径永远不会被输出(只暴露 tls_enabled 布尔值),而 internal_addr 和 error_pages_dir 也会从所返回的响应中被清除——这些部署拓扑和文件系统路径对攻击者有帮助,而抓取程序并不需要它们。

插件端点

插件可以在 /__<plugin_name>/ 前缀下注册自定义端点。例如,一个名为 otel 的插件可以暴露 /__otel/status。只有在对应的插件已加载并注册了处理器时,这些端点才可用。

任何既不匹配内置端点也不匹配插件端点的路径都会返回 404 Not Found。

Kubernetes 集成

请将每个探针接到专为它设计的端点,而不是让三个探针都指向 /health。该端点会把所有子系统合并成一个状态,因此若用于存活探针,插件故障或工作池停滞都会以重启 Pod 作为回应。

就绪探针

/health/readiness 控制 Kubernetes 是否将流量路由到该 Pod:

yaml
readinessProbe: httpGet: path: /health/readiness port: 9090 initialDelaySeconds: 2 periodSeconds: 5

当它返回 503 时,Kubernetes 会将该 Pod 从 Service 的端点列表中移除;当它重新返回 200 时,流量恢复。它报告的是故障而不是负载,因此过载的 Pod 仍会留在轮转中。参见 Health Checks。

存活探针

只要进程还能处理 HTTP 请求,/health/liveness 就会返回 200。这正是存活探针应有的含义,因为重启无法修复过载的工作池,反而会把正在处理的工作一并丢掉:

yaml
livenessProbe: httpGet: path: /health/liveness port: 9090 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3

启动探针

适用于启动较慢的应用(大型框架、繁重的自动加载器):

yaml
startupProbe: httpGet: path: /health/startup port: 9090 initialDelaySeconds: 1 periodSeconds: 2 failureThreshold: 15

安全

内部服务器没有 bearer-token 认证;访问控制取决于它绑定在何处以及 INTERNAL_ALLOW_IPS。要保护它:

  • 绑定到 localhost — 仅指定端口的 INTERNAL_ADDR(:9090)已经会绑定到 127.0.0.1;显式的 127.0.0.1:9090 可让该端口仅能从容器或主机内部访问
  • 设置 INTERNAL_ALLOW_IPS — 当监听器必须能从主机之外访问时使用它——这是一个 CIDR/IP 允许列表,对列表之外的对端在访问 /metrics、/config 以及插件路径时返回 403,同时健康探针保持可达。回环地址不会被隐式包含,因此如果你仍需要 localhost 访问,请加入 127.0.0.1/32。如果监听器暴露到主机之外却未设置允许列表,服务器会在启动时发出警告
  • 不要将其作为 Kubernetes Service 暴露 — 将该端口声明为 containerPort,但不要为它创建 Service。Kubernetes 探针会直接访问容器端口
  • 使用网络策略 — 如果该端口必须暴露,请在网络层限制访问
Warning

/config 端点会泄露运维细节(文档根目录、限流参数、工作进程数量、超时值)。TLS 路径、internal_addr 和 error_pages_dir 会被清除,但仍需考虑其余内容是否应当能从 Pod 外部访问。

Docker 示例

要快速查看,你可以将内部端口发布出来;在生产环境中,请将内部服务器绑定到 localhost;若运行在 Kubernetes 下,由于其 httpGet 探针无法访问回环监听器,则应绑定 0.0.0.0:9090 并设置 INTERNAL_ALLOW_IPS。

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "80:80" - "9090:9090" environment: - DOCUMENT_ROOT=/var/www/html/public - INTERNAL_ADDR=0.0.0.0:9090

故障排查

健康检查、指标和配置端点不可用

INTERNAL_ADDR 未设置。

解决方法: 添加该环境变量:

bash
INTERNAL_ADDR=0.0.0.0:9090
/health 返回 503 但应用正常工作

某个已加载的插件正在报告 PluginHealth::Failed;较少见的情况是 executor_healthy 为 false 或 pool_stalled 为 true。检查健康响应中的 plugins 对象以确定是哪个插件出了问题:

bash
curl -s http://localhost:9090/health | jq '.plugins'
无法从容器外部访问内部服务器

该服务器绑定到了 127.0.0.1,只能从容器内部访问。

解决方法: 改为 0.0.0.0 以供外部访问,并设置 INTERNAL_ALLOW_IPS。Kubernetes 的 httpGet 探针也需要同样的修改,因为它们无法访问回环监听器。

指标中未显示工作进程模式或异步指标

工作进程模式的指标只有在启用了工作进程模式(WORKER_MODE_ENABLED=true)时才会出现。异步指标只有在 ASYNC_WORKERS > 0 且至少有一个任务被派发或被拒绝时才会出现。

另请参阅

发现错误?报告问题 →