优雅关闭
OxPHP 会处理 SIGTERM 和 SIGINT,以便进程退出前先让正在处理的请求完成。这对于零停机部署以及容器编排中的滚动更新至关重要。
信号处理
OxPHP 响应两种关闭信号:
| 信号 | 来源 | 行为 |
|---|---|---|
SIGTERM |
容器编排器、docker stop、kill |
发起优雅关闭 |
SIGINT |
终端 Ctrl+C | 发起优雅关闭 |
两种信号都会触发相同的关闭流程。只需要第一个信号即可,服务器会立即开始排空。
关闭流程
收到关闭信号后,OxPHP 会遵循以下流程:
- 停止接收新连接:服务器停止在主端口上接受连接,但监听套接字本身会一直保持打开,直到进程退出。因此,在排空期间发起连接的客户端会完成 TCP 握手,却永远得不到应答:它会一直等到自己超时,或者等到进程退出并重置该连接。只有进程退出之后发起的连接才会被直接拒绝,所以请在信号到达实例之前先将其移出轮转(参见 Kubernetes)。PHP 工作进程继续运行,以处理正在进行中的请求。
- 收尾活跃连接 —— HTTP/2 客户端会收到一个
GOAWAY帧,空闲的 HTTP/1.1 keep-alive 连接会被关闭,这样客户端就会转移到健康的实例,而不是把新请求多路复用到即将关闭的实例上。已打开的流会被迅速且干净地结束 —— 任何已经开始刷新分块输出的响应都算在内,既包括有限长度的下载,也包括 SSE —— 参见 Server-Sent Events。 - 排空正在处理的请求 —— 普通的活跃请求不受打扰,可以带着完整响应正常结束。服务器每 100ms 检查一次是否完成。内部的健康/指标服务器在整个排空期间保持可用,因此就绪探针能够继续工作。请求在
oxphp_finish_request()之后继续做的工作同样算作在途,即便它的连接早已不在了。 - 强制执行排空截止时间:超过
DRAIN_TIMEOUT_SECONDS后仍在运行的请求会被取消(它们的register_shutdown_function()回调仍会执行),并被给予约 2 秒来收尾,之后服务器继续推进。处于阻塞调用(sleep()、数据库查询、套接字读取)中的请求无法在该调用内部被中断。在传统模式下始终如此;在工作进程模式下,除非RUNTIME_HOOKS已将该调用变成纤程挂起,否则也是如此。它的连接会按时关闭,但它的工作线程——以及整个进程——只有在该调用返回后才会退出。因此,这约 2 秒并不是进程退出时间的上限。 - 刷新插件 —— 排空窗口期间缓冲的访问日志条目和 APM span 会被刷新。
- 关闭异步池 —— 后台异步任务池被停止。
- 中止内部服务器 —— 排空完成后,健康/指标服务器被停止。
- 退出 —— 进程以状态码 0 退出。
第 1 步和第 2 步在信号到达的那一刻就开始;而排空截止时间只有在接受循环停止之后才开始计时。如果信号到达时 MAX_CONNECTIONS 允许的连接已全部占满,接受循环正在等待某个连接关闭,只有到那时才会停止。在有连接关闭之前,截止时间不会开始计时,第 4 步也不会取消任何仍在运行的工作。如果占满配额的是空闲的 keep-alive 连接,第 2 步会关闭它们,这一阶段转瞬即过;如果占满配额的是长请求或慢客户端,这一阶段会持续到它们结束为止,并可能耗尽编排器的整个宽限期。
PHP 工作线程并不会在流程开始时就被停止。它们是在最后一刻——排空结束、进程即将退出时——才被通知退出并被 join。这正是为什么即使已经没有任何客户端连接,排空也必须把仍在工作线程上运行的工作计算在内。
配置
| 变量 | 默认值 | 说明 |
|---|---|---|
DRAIN_TIMEOUT_SECONDS |
25 |
正在处理的请求被取消前可用于完成的最大秒数;截止时间之后约 2 秒,服务器停止提供服务;除非有请求处于阻塞调用中(见上文第 4 步),否则进程也在此时退出。默认值为截止后的收尾和遥测刷新预留了余量,使其能落在 Kubernetes 默认的 30 秒终止宽限期之内 |
将 DRAIN_TIMEOUT_SECONDS 设置为能容纳你预期最慢的请求:
- API 服务器,响应较快:
10–15秒 - 应用程序,涉及文件上传或长查询:
30–60秒 - 工作进程模式,涉及后台处理:与你预期最长的操作相匹配
Kubernetes
在 Kubernetes 中,滚动更新期间的关闭流程如下:
- Kubernetes 向 pod 发送
SIGTERM。 - pod 从 Service 端点列表中被移除。
- OxPHP 在
DRAIN_TIMEOUT_SECONDS之内排空正在处理的连接,然后取消掉落队者,并在约 2 秒后停止提供服务。进程在此时退出,或者在工作线程上最后一个阻塞调用返回时退出。 - 如果 pod 在
terminationGracePeriodSeconds之后仍在运行,Kubernetes 会发送SIGKILL。
将 terminationGracePeriodSeconds 设置为高于 DRAIN_TIMEOUT_SECONDS + 2,再加上截止时间到达时请求可能所处的最长阻塞调用(即应用程序允许的最长查询或套接字超时),这样排空(包括截止后的收尾和遥测刷新)就能在强制终止前完成。如果只加 + 2,截止时间到达时仍卡在长查询中的请求会导致 pod 被 SIGKILL 杀死(退出码 137),而不是以 0 退出:
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"预停止钩子
如果你的服务从传播端点变更较慢的外部负载均衡器接收流量,可以添加一个预停止钩子来延迟关闭流程:
lifecycle:
preStop:
exec:
command: ["sleep", "5"]这样负载均衡器就有时间在 OxPHP 停止接受连接之前,把该 pod 从其目标列表中移除。这一点很重要,因为信号之后被路由到该 pod 的连接不会被拒绝,而是在排空剩余时间里一直得不到应答(参见关闭流程的第 1 步)。
Docker
运行 docker stop 时,Docker 会发送 SIGTERM。Docker 默认的停止超时为 10 秒,超过后 Docker 会发送 SIGKILL。
为了给 OxPHP 足够的时间排空,请增大停止超时:
docker stop --time 45 my-oxphp-container或者在你的 Compose 文件中设置:
services:
oxphp:
image: ghcr.io/oxphp/oxphp:0.13.0
stop_grace_period: 45s
environment:
DRAIN_TIMEOUT_SECONDS: "30"日志消息
在优雅关闭期间,OxPHP 会输出结构化日志消息,你可以对其进行监控:
排空成功:
{"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 中——它的后台工作还在运行时,连接已经不在了。
达到排空截止时间:
{"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 服务器——需要哪些页面它会自己去读。
让我的 OxPHP 部署在关闭时不丢请求。通过 OxPHP 文档 MCP 服务器读取 `operations/graceful-shutdown` 页面。
把我的 Kubernetes 清单里的终止宽限期、预停止钩子和关闭超时设成彼此一致的值,并把我的应用程序允许的最长阻塞调用(查询或套接字超时)考虑在内,然后告诉我每一步里正在处理的请求会怎么样。