优雅关闭

OxPHP 会处理 SIGTERM 和 SIGINT,以便进程退出前先让正在处理的请求完成。这对于零停机部署以及容器编排中的滚动更新至关重要。

信号处理

OxPHP 响应两种关闭信号:

信号 来源 行为
SIGTERM 容器编排器、docker stop、kill 发起优雅关闭
SIGINT 终端 Ctrl+C 发起优雅关闭

两种信号都会触发相同的关闭流程。只需要第一个信号即可,服务器会立即开始排空。

关闭流程

收到关闭信号后,OxPHP 会遵循以下流程:

  1. 停止接收新连接:服务器停止在主端口上接受连接,但监听套接字本身会一直保持打开,直到进程退出。因此,在排空期间发起连接的客户端会完成 TCP 握手,却永远得不到应答:它会一直等到自己超时,或者等到进程退出并重置该连接。只有进程退出之后发起的连接才会被直接拒绝,所以请在信号到达实例之前先将其移出轮转(参见 Kubernetes)。PHP 工作进程继续运行,以处理正在进行中的请求。
  2. 收尾活跃连接 —— HTTP/2 客户端会收到一个 GOAWAY 帧,空闲的 HTTP/1.1 keep-alive 连接会被关闭,这样客户端就会转移到健康的实例,而不是把新请求多路复用到即将关闭的实例上。已打开的流会被迅速且干净地结束 —— 任何已经开始刷新分块输出的响应都算在内,既包括有限长度的下载,也包括 SSE —— 参见 Server-Sent Events。
  3. 排空正在处理的请求 —— 普通的活跃请求不受打扰,可以带着完整响应正常结束。服务器每 100ms 检查一次是否完成。内部的健康/指标服务器在整个排空期间保持可用,因此就绪探针能够继续工作。请求在 oxphp_finish_request() 之后继续做的工作同样算作在途,即便它的连接早已不在了。
  4. 强制执行排空截止时间:超过 DRAIN_TIMEOUT_SECONDS 后仍在运行的请求会被取消(它们的 register_shutdown_function() 回调仍会执行),并被给予约 2 秒来收尾,之后服务器继续推进。处于阻塞调用(sleep()、数据库查询、套接字读取)中的请求无法在该调用内部被中断。在传统模式下始终如此;在工作进程模式下,除非 RUNTIME_HOOKS 已将该调用变成纤程挂起,否则也是如此。它的连接会按时关闭,但它的工作线程——以及整个进程——只有在该调用返回后才会退出。因此,这约 2 秒并不是进程退出时间的上限。
  5. 刷新插件 —— 排空窗口期间缓冲的访问日志条目和 APM span 会被刷新。
  6. 关闭异步池 —— 后台异步任务池被停止。
  7. 中止内部服务器 —— 排空完成后,健康/指标服务器被停止。
  8. 退出 —— 进程以状态码 0 退出。
Note

第 1 步和第 2 步在信号到达的那一刻就开始;而排空截止时间只有在接受循环停止之后才开始计时。如果信号到达时 MAX_CONNECTIONS 允许的连接已全部占满,接受循环正在等待某个连接关闭,只有到那时才会停止。在有连接关闭之前,截止时间不会开始计时,第 4 步也不会取消任何仍在运行的工作。如果占满配额的是空闲的 keep-alive 连接,第 2 步会关闭它们,这一阶段转瞬即过;如果占满配额的是长请求或慢客户端,这一阶段会持续到它们结束为止,并可能耗尽编排器的整个宽限期。

Note

PHP 工作线程并不会在流程开始时就被停止。它们是在最后一刻——排空结束、进程即将退出时——才被通知退出并被 join。这正是为什么即使已经没有任何客户端连接,排空也必须把仍在工作线程上运行的工作计算在内。

配置

变量 默认值 说明
DRAIN_TIMEOUT_SECONDS 25 正在处理的请求被取消前可用于完成的最大秒数;截止时间之后约 2 秒,服务器停止提供服务;除非有请求处于阻塞调用中(见上文第 4 步),否则进程也在此时退出。默认值为截止后的收尾和遥测刷新预留了余量,使其能落在 Kubernetes 默认的 30 秒终止宽限期之内

将 DRAIN_TIMEOUT_SECONDS 设置为能容纳你预期最慢的请求:

  • API 服务器,响应较快:10–15 秒
  • 应用程序,涉及文件上传或长查询:30–60 秒
  • 工作进程模式,涉及后台处理:与你预期最长的操作相匹配

Kubernetes

在 Kubernetes 中,滚动更新期间的关闭流程如下:

  1. Kubernetes 向 pod 发送 SIGTERM。
  2. pod 从 Service 端点列表中被移除。
  3. OxPHP 在 DRAIN_TIMEOUT_SECONDS 之内排空正在处理的连接,然后取消掉落队者,并在约 2 秒后停止提供服务。进程在此时退出,或者在工作线程上最后一个阻塞调用返回时退出。
  4. 如果 pod 在 terminationGracePeriodSeconds 之后仍在运行,Kubernetes 会发送 SIGKILL。

将 terminationGracePeriodSeconds 设置为高于 DRAIN_TIMEOUT_SECONDS + 2,再加上截止时间到达时请求可能所处的最长阻塞调用(即应用程序允许的最长查询或套接字超时),这样排空(包括截止后的收尾和遥测刷新)就能在强制终止前完成。如果只加 + 2,截止时间到达时仍卡在长查询中的请求会导致 pod 被 SIGKILL 杀死(退出码 137),而不是以 0 退出:

yaml
apiVersion: apps/v1 kind: Deployment spec: template: spec: terminationGracePeriodSeconds: 45 containers: - name: oxphp image: ghcr.io/oxphp/oxphp:0.13.0 env: - name: DRAIN_TIMEOUT_SECONDS value: "30"

预停止钩子

如果你的服务从传播端点变更较慢的外部负载均衡器接收流量,可以添加一个预停止钩子来延迟关闭流程:

yaml
lifecycle: preStop: exec: command: ["sleep", "5"]

这样负载均衡器就有时间在 OxPHP 停止接受连接之前,把该 pod 从其目标列表中移除。这一点很重要,因为信号之后被路由到该 pod 的连接不会被拒绝,而是在排空剩余时间里一直得不到应答(参见关闭流程的第 1 步)。

Docker

运行 docker stop 时,Docker 会发送 SIGTERM。Docker 默认的停止超时为 10 秒,超过后 Docker 会发送 SIGKILL。

为了给 OxPHP 足够的时间排空,请增大停止超时:

bash
docker stop --time 45 my-oxphp-container

或者在你的 Compose 文件中设置:

compose.yaml
services: oxphp: image: ghcr.io/oxphp/oxphp:0.13.0 stop_grace_period: 45s environment: DRAIN_TIMEOUT_SECONDS: "30"

日志消息

在优雅关闭期间,OxPHP 会输出结构化日志消息,你可以对其进行监控:

排空成功:

json
{"level":"INFO","message":"Received shutdown signal, draining connections"} {"level":"INFO","message":"Draining in-flight connections","active_connections":3,"in_flight_requests":4} {"level":"INFO","message":"All connections drained"} {"level":"INFO","message":"Server stopped"}

这两个计数是重叠关系而非互斥划分:active_connections 是仍然存活的客户端连接,in_flight_requests 是仍在 PHP 工作线程上执行的请求,而一个占着连接的请求会被两者同时计入。用 oxphp_finish_request() 提前结束响应的请求只出现在 in_flight_requests 中——它的后台工作还在运行时,连接已经不在了。

达到排空截止时间:

json
{"level":"INFO","message":"Received shutdown signal, draining connections"} {"level":"WARN","message":"Drain timeout reached, cancelling in-flight requests","remaining_connections":1,"in_flight_requests":1} {"level":"INFO","message":"All connections drained"} {"level":"INFO","message":"Server stopped"}

如果你经常看到 “Drain timeout reached” 警告,请增大 DRAIN_TIMEOUT_SECONDS,或使用 oxphp_request_duration_us 直方图排查长时间运行的请求。

给你的智能体的提示词

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

prompt
让我的 OxPHP 部署在关闭时不丢请求。通过 OxPHP 文档 MCP 服务器读取 `operations/graceful-shutdown` 页面。 把我的 Kubernetes 清单里的终止宽限期、预停止钩子和关闭超时设成彼此一致的值,并把我的应用程序允许的最长阻塞调用(查询或套接字超时)考虑在内,然后告诉我每一步里正在处理的请求会怎么样。

另请参阅

  • 健康检查 —— 就绪探针与关闭的交互
  • 配置参考 —— 包括 DRAIN_TIMEOUT_SECONDS 在内的所有环境变量
  • 指标 —— oxphp_active_connections 追踪排空期间的连接数
发现错误?报告问题 →