超时

OxPHP 强制执行两个相互独立的超时,用于防范慢速客户端和失控的请求。请求头超时在服务器层面保护连接建立阶段。PHP 的执行时间则由 PHP 自身的 max_execution_time ini 指令(以及 set_time_limit() 运行时函数)来限制。该限制有一个特性与 PHP-FPM 不同,而且很容易被忽略:OxPHP 运行的是线程安全(ZTS)的 PHP 构建,在这种构建中,该限制计量的是挂钟时间,而不是 CPU 时间。参见挂钟时间,而非 CPU 时间。

工作原理

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

  1. 连接被接受 — 请求头超时开始计时。OxPHP 会等待客户端发送一整套完整的 HTTP 请求头。
  2. 收到请求头 — 请求头超时结束。请求被分派给一个 PHP 工作进程。
  3. PHP 处理请求 — 应用代码在 PHP 自身的 max_execution_time 限制下运行,该限制以每线程 POSIX 定时器的形式设置。当达到该限制时,请求会被取消,并触发统一的 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) 消息写入错误日志。

挂钟时间,而非 CPU 时间

max_execution_time 计量的是请求已经存活了多久,而不是它消耗了多少 CPU。每一秒等待都会被计入:一次缓慢的数据库查询、一次对其他服务的 HTTP 调用、一次在套接字上阻塞的 fread()、一次 sleep()。

这一点与 PHP-FPM 不同,而差异来自 PHP 构建,而不是 OxPHP。线程安全(ZTS)构建会把该限制设置为基于单调时钟的每线程 POSIX 定时器;OxPHP 必须使用这种构建,因为它以同一进程内多个线程的形式运行多个 PHP 解释器。非线程安全构建(也就是 PHP-FPM 所运行的构建)则用 setitimer(ITIMER_PROF) 来设置它,这种定时器只在进程占用 CPU 时才会推进。那条人们熟悉的经验法则"等待数据库的时间不计入 max_execution_time"正是由此而来。它对 PHP-FPM 成立,在这里并不成立。

迁移现有应用时,其后果就会显现出来:

请求:等待报表查询 25 秒,渲染 2 秒 计入 30 秒限制的时间
PHP-FPM(非线程安全构建) 约 2 秒(该限制实际上永远达不到)
OxPHP(线程安全构建) 27 秒(一次慢查询就可能耗尽该限制)
从 PHP-FPM 迁移

一个在 PHP-FPM 下从未触及 max_execution_time 的应用,在迁移到 OxPHP 之后,即使该指令的值没有任何改动,也可能开始在最慢的端点上返回 504。请按请求的总延迟(包括请求所等待的一切)来确定该限制,并为阻塞在外部系统上的端点调高它。

取消状态码

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

原因 状态码 说明
超过 max_execution_time / set_time_limit() 504 Gateway Timeout 服务器端执行时间耗尽。
服务器优雅关闭时清空了该请求 503 Service Unavailable 会附加 Retry-After: 5,让客户端向已恢复或替换的实例重试。
客户端在请求进行中关闭了连接 499 nginx 风格的 "Client Closed Request"。此时连接已经断开,因此该状态码只会出现在访问日志和指标中 —— 它永远不会被写入传输。它以非 5xx 的形式呈现由客户端主动中止的情况,从而不会污染服务器错误告警。

只有这三种取消原因。监控进程观察到运行时间超过其阈值的请求,会通过 oxphp_worker_stuck_total 和 oxphp_worker_long_running_total 上报,并任其继续运行;服务器永远不会取消它。

如果你的 ERROR_PAGES_DIR 只提供了 500.html,请再添加 504.html、503.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 执行时间限制在脚本完成之前就触发了。

如果这些请求在 PHP-FPM 下一切正常,迁移到 OxPHP 之后才开始失败,那么原因很可能就是挂钟时间,而非 CPU 时间中描述的定时器语义:请求等待数据库或上游服务所花的时间会计入该限制。

解决方法: 为受影响的脚本提高 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.13.0 ports: - "8080:80" 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 能够快速捕获卡死的请求,同时又不影响正常流量。请按最慢的下游依赖来确定它,而不是按脚本所做的 CPU 工作,因为等待的时间也会被计入。
  • 与限流结合使用。 超时保护的是单个请求;限流则防范高请求量。两者结合起来,就能同时覆盖慢速和快速两种攻击模式。

给你的智能体的提示词

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

prompt
按我最长的那个请求来设置 OxPHP 的超时。通过 OxPHP 文档 MCP 服务器读取 `features/timeouts` 页面。 找出这个仓库里实际跑得最久的东西(上传、报表、导出,以及对数据库和上游服务的等待,因为该限制计的是挂钟时间),然后给我该用的请求头超时和 `max_execution_time`,外加那些没法活在全局上限之下的脚本所需的按脚本覆盖设置。

另请参阅

发现错误?报告问题 →