超时
OxPHP 强制执行两个相互独立的超时,用于防范慢速客户端和失控的请求。请求头超时在服务器层面保护连接建立阶段。PHP 的执行时间则由 PHP 自身的 max_execution_time ini 指令(以及 set_time_limit() 运行时函数)来限制。该限制有一个特性与 PHP-FPM 不同,而且很容易被忽略:OxPHP 运行的是线程安全(ZTS)的 PHP 构建,在这种构建中,该限制计量的是挂钟时间,而不是 CPU 时间。参见挂钟时间,而非 CPU 时间。
工作原理
每个请求都会经过以下几个阶段:
- 连接被接受 — 请求头超时开始计时。OxPHP 会等待客户端发送一整套完整的 HTTP 请求头。
- 收到请求头 — 请求头超时结束。请求被分派给一个 PHP 工作进程。
- PHP 处理请求 — 应用代码在 PHP 自身的
max_execution_time限制下运行,该限制以每线程 POSIX 定时器的形式设置。当达到该限制时,请求会被取消,并触发统一的Request cancelled (timeout)致命错误。在工作进程模式下,每个请求都保有自己的时限,包括工作进程在承载其他请求期间接收的请求;时限耗尽时正挂起在某个等待中的请求,会在恢复执行时被结束(参见工作进程模式 → 请求截止时间)。 - 响应已发送 — 在 keep-alive 连接上,整个循环会从第 1 步重新开始。
graph TD A["TCP 连接<br/>(若启用则含 TLS 握手)"] -->|HEADER_TIMEOUT_SECONDS| B["收到请求头"] B -->|max_execution_time| C["响应已发送"] C -->|下一个请求(keep-alive)| A
在 keep-alive 连接上,这两个超时会分别独立地应用于该连接中的每个请求。
当启用 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 下从未触及 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
max_execution_time = 30或者在运行时按脚本设置:
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) 来延长它:
// at the top of a slow script
set_time_limit(300);对于连接必须无限期保持打开的 SSE 或流式端点,请为该脚本禁用计时器:
set_time_limit(0);请求头到达之前连接就被断开
对于处于高延迟链路上或位于慢速负载均衡器之后的客户端来说,请求头超时设置得太短了。
解决方法: 增大请求头超时:
HEADER_TIMEOUT_SECONDS=15OPcache 导致脚本变更后的首个请求超时
文件变更后,OPcache 的重新编译会给首个请求增加延迟。在文件数量众多的开发环境中,这种情况更为常见。可在开发期间提高 max_execution_time 或将其禁用。
Docker 示例
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 包含:
max_execution_time = 30最佳实践
- 切勿在生产环境中全局设置
max_execution_time = 0,除非你有需要无限期连接的 SSE 或长轮询端点。请优先按脚本使用set_time_limit(0)。 - 为 API 服务器使用更短的限制。 API 的响应时间是可预测的。30 秒的
max_execution_time能够快速捕获卡死的请求,同时又不影响正常流量。请按最慢的下游依赖来确定它,而不是按脚本所做的 CPU 工作,因为等待的时间也会被计入。 - 与限流结合使用。 超时保护的是单个请求;限流则防范高请求量。两者结合起来,就能同时覆盖慢速和快速两种攻击模式。
给你的智能体的提示词
这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。
按我最长的那个请求来设置 OxPHP 的超时。通过 OxPHP 文档 MCP 服务器读取 `features/timeouts` 页面。
找出这个仓库里实际跑得最久的东西(上传、报表、导出,以及对数据库和上游服务的等待,因为该限制计的是挂钟时间),然后给我该用的请求头超时和 `max_execution_time`,外加那些没法活在全局上限之下的脚本所需的按脚本覆盖设置。另请参阅
- 限流 — 按 IP 的请求限流
- 服务器发送事件(SSE) — 关于为流式端点禁用超时的指导
- 配置参考 — 环境变量的完整参考