超时
OxPHP 强制执行两个相互独立的超时,用于防范慢速客户端和失控的请求。请求头超时在服务器层面保护连接建立阶段。PHP 的执行时间则由 PHP 自身的 max_execution_time ini 指令(以及 set_time_limit() 运行时函数)来限制,与在任何其他 SAPI 上完全一致。
工作原理
每个请求都会经过以下几个阶段:
- 连接被接受 — 请求头超时开始计时。OxPHP 会等待客户端发送一整套完整的 HTTP 请求头。
- 收到请求头 — 请求头超时结束。请求被分派给一个 PHP 工作进程。
- PHP 处理请求 — 应用代码在 PHP 自身的
max_execution_time(由 SIGALRM 驱动)限制下运行。当达到该限制时,请求会被取消,并触发统一的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)消息写入错误日志。
取消状态码
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.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 执行时间限制在脚本完成之前就触发了。
解决方法: 为受影响的脚本提高 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.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 包含:
max_execution_time = 30最佳实践
- 切勿在生产环境中全局设置
max_execution_time = 0,除非你有需要无限期连接的 SSE 或长轮询端点。请优先按脚本使用set_time_limit(0)。 - 为 API 服务器使用更短的限制。 API 的响应时间是可预测的。30 秒的
max_execution_time能够快速捕获卡死的请求,同时又不影响正常流量。 - 与限流结合使用。 超时保护的是单个请求;限流则防范高请求量。两者结合起来,就能同时覆盖慢速和快速两种攻击模式。
另请参阅
- 限流 — 按 IP 的请求限流
- 服务器发送事件(SSE) — 关于为流式端点禁用超时的指导
- 配置参考 — 环境变量的完整参考