超时

OxPHP 强制执行两个相互独立的超时,用于防范慢速客户端和失控的请求。请求头超时在服务器层面保护连接建立阶段。PHP 的执行时间则由 PHP 自身的 max_execution_time ini 指令(以及 set_time_limit() 运行时函数)来限制,与在任何其他 SAPI 上完全一致。

工作原理

每个请求都会经过以下几个阶段:

  1. 连接被接受 — 请求头超时开始计时。OxPHP 会等待客户端发送一整套完整的 HTTP 请求头。
  2. 收到请求头 — 请求头超时结束。请求被分派给一个 PHP 工作进程。
  3. PHP 处理请求 — 应用代码在 PHP 自身的 max_execution_time(由 SIGALRM 驱动)限制下运行。当达到该限制时,请求会被取消,并触发统一的 Request cancelled (timeout) 致命错误。
  4. 响应已发送 — 在 keep-alive 连接上,整个循环会从第 1 步重新开始。
graph TD
  A["TCP 连接<br/>(若启用则含 TLS 握手)"] -->|HEADER_TIMEOUT_SECONDS| B["收到请求头"]
  B -->|max_execution_time| C["响应已发送"]
  C -->|下一个请求(keep-alive)| A

在 keep-alive 连接上,这两个超时会分别独立地应用于该连接中的每个请求。

Note

当启用 TLS 时,请求头超时会在 TLS 握手完成之后开始计时,而不是在 TCP 连接被接受时开始。

请求头超时可防范 slowloris 类型的攻击 —— 在这类攻击中,客户端会以每次一个字节的方式发送请求头,从而无限期地占用连接。

PHP 的执行计时完全委托给 PHP 处理。当超过 max_execution_time 时,OxPHP 的统一取消流程会:

  • 设置 connection_status() & PHP_CONNECTION_TIMEOUT,以便用户代码能够检测出原因。
  • 运行所有 register_shutdown_function() 回调,与 PHP-FPM 的行为完全一致。
  • 返回 HTTP 504 Gateway Timeout,并将 Request cancelled (timeout) 消息写入错误日志。

取消状态码

OxPHP 会因几种不同的原因取消请求。每种原因都会映射到一个能反映实际情况的传输状态码,而不是笼统的 500

原因 状态码 说明
超过 max_execution_time / set_time_limit() 504 Gateway Timeout 服务器端执行时间耗尽。
服务器优雅关闭时清空了该请求 503 Service Unavailable 会附加 Retry-After: 5,让客户端向已恢复或替换的实例重试。
客户端在请求进行中关闭了连接 499 nginx 风格的 "Client Closed Request"。此时连接已经断开,因此该状态码只会出现在访问日志和指标中 —— 它永远不会被写入传输。它以非 5xx 的形式呈现由客户端主动中止的情况,从而不会污染服务器错误告警。
工作进程被监控进程判定为卡死 500 Internal Server Error 通用的服务器错误 —— 具体原因(死锁、阻塞的系统调用等)未知。
由用户代码发起的取消 500 Internal Server Error 用户代码可以在触发取消之前用 http_response_code() 设置自己的状态码;该显式设置的状态码会被保留。

如果你的 ERROR_PAGES_DIR 只提供了 500.html,请再添加 504.html503.html,以及(可选的)499.html,以便在各种取消原因下都能保持样式统一的页面。

配置

变量 默认值 说明
HEADER_TIMEOUT_SECONDS 5 连接被接受后接收请求头的最长秒数。可防范 slowloris 攻击。0 不会被特殊对待 —— 它会作为零秒超时透传给 hyper,从而立即触发。若要禁用该超时,应取消设置此变量,而不是将其设为 0

PHP 执行时间通过 php.ini 配置,而不是通过 OxPHP 的环境变量:

php.ini
; php.ini max_execution_time = 30

或者在运行时按脚本设置:

php
set_time_limit(60); // 60 seconds from now set_time_limit(0); // disable for this request

推荐值

场景 请求头超时 max_execution_time
API 服务器 5 秒 30 秒
通用网页服务 5 秒 60 秒
文件上传 10 秒 300 秒
SSE / 长轮询 5 秒 0(禁用,按脚本设置)

请根据你的应用特性来调整这些值。对于 SSE 端点,应在流式脚本的开头调用 set_time_limit(0),而不是在全局范围内禁用 max_execution_time

故障排查

客户端意外收到带有 "Request cancelled (timeout)" 的 504

PHP 执行时间限制在脚本完成之前就触发了。

解决方法: 为受影响的脚本提高 max_execution_time,或在运行时调用 set_time_limit($seconds) 来延长它:

php
// at the top of a slow script set_time_limit(300);

对于连接必须无限期保持打开的 SSE 或流式端点,请为该脚本禁用计时器:

php
set_time_limit(0);
请求头到达之前连接就被断开

对于处于高延迟链路上或位于慢速负载均衡器之后的客户端来说,请求头超时设置得太短了。

解决方法: 增大请求头超时:

bash
HEADER_TIMEOUT_SECONDS=15
OPcache 导致脚本变更后的首个请求超时

文件变更后,OPcache 的重新编译会给首个请求增加延迟。在文件数量众多的开发环境中,这种情况更为常见。可在开发期间提高 max_execution_time 或将其禁用。

Docker 示例

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.0 ports: - "8080:8080" environment: HEADER_TIMEOUT_SECONDS: "5" volumes: - ./app:/var/www/html:ro - ./php.ini:/usr/local/etc/php/conf.d/zz-app.ini:ro

其中 php.ini 包含:

php.ini
max_execution_time = 30

最佳实践

  • 切勿在生产环境中全局设置 max_execution_time = 0,除非你有需要无限期连接的 SSE 或长轮询端点。请优先按脚本使用 set_time_limit(0)
  • 为 API 服务器使用更短的限制。 API 的响应时间是可预测的。30 秒的 max_execution_time 能够快速捕获卡死的请求,同时又不影响正常流量。
  • 与限流结合使用。 超时保护的是单个请求;限流则防范高请求量。两者结合起来,就能同时覆盖慢速和快速两种攻击模式。

另请参阅