工作进程模式

工作进程模式运行常驻的 PHP 进程,这些进程只引导一次,然后处理大量请求,因此启动 PHP 的开销只需支付一次,而不是每个请求都支付。你的应用无需在每个请求时销毁并重建 PHP 状态,而是只加载一次自动加载器、配置和数据库连接,并在工作进程的整个生命周期内复用它们。

工作原理

  1. 启用工作进程模式。 设置 WORKER_MODE_ENABLED=true 并让 ENTRY_FILE 指向你的引导脚本。这会为进程池中的所有 PHP 工作进程启用工作进程模式。
  2. 只引导一次。 PHP 启动并运行外层作用域一次。自动加载器注册、配置加载、数据库连接以及任何其他初始化代码都只执行一次。
  3. 进入请求循环。 调用 oxphp_worker(callback)。OxPHP 开始将传入的 HTTP 请求分发给你的回调函数。
  4. 在请求之间重置。 超全局变量($_GET$_POST$_SERVER$_COOKIE$_FILESphp://input)、输出缓冲区和响应头会自动重置。软重置会清理每个请求的状态,同时保留外层作用域中已引导的资源。
  5. 外层作用域持续存在。oxphp_worker() 之前定义的变量、静态属性、数据库连接和自动加载器,在该工作进程处理的所有请求之间都保持可用。
Note

工作进程模式会改变路由行为。所有未匹配到磁盘上静态文件的请求都会被分发给工作进程,而不是返回 404。详见路由

配置

变量 默认值 说明
WORKER_MODE_ENABLED false 启用常驻工作进程模式。接受 true1yes。要求 ENTRY_FILE 指向一个 .php 脚本
ENTRY_FILE (未设置) 工作进程引导脚本的路径。相对路径时会相对于 DOCUMENT_ROOT 解析;允许使用 .. 片段和绝对路径(工作进程引导脚本位于公共文档根目录之外是一种受支持的布局)
WORKER_MAX_MEMORY_MIB 0 每个工作进程回收前的最大 PHP 内存(以 MiB 为单位)。0 = 无限制
从 WORKER_FILE 迁移

遗留的 WORKER_FILE 变量仍会被解析(并伴随一条启动时的 WARN),其行为等同于 WORKER_MODE_ENABLED=true ENTRY_FILE=$WORKER_FILE。新的部署应使用这一对显式变量;遗留形式将在未来版本中移除。

对于由应用驱动的回收,可在请求处理函数内部调用 OxPHP\Server\Worker::scheduleExit()。工作进程会在当前请求完成后干净地退出。

编写工作进程脚本

工作进程脚本有两部分:在启动时运行一次的外层作用域,以及传递给 oxphp_worker() 的在每个请求时运行的回调函数。

worker.php
<?php // Outer scope: runs once at startup require __DIR__ . '/../vendor/autoload.php'; $config = parse_ini_file(__DIR__ . '/../config/app.ini'); $db = new PDO($config['dsn'], $config['user'], $config['pass'], [ PDO::ATTR_PERSISTENT => true, ]); $app = new MyApp\Application($config, $db); // Request loop: runs for every request oxphp_worker(function () use ($app) { $app->handle(); }); // Shutdown: runs when the worker exits $app->terminate();

哪些会重置,哪些会持续存在

OxPHP 会在请求之间执行软重置。每个请求的状态会被自动清理,而在外层作用域中引导的任何内容都会在工作进程的整个生命周期内存活。

在请求之间重置
  • 超全局变量$_GET$_POST$_SERVER$_COOKIE$_FILESphp://input 会用新的请求数据重新填充
  • 输出缓冲区 — 所有输出缓冲区都会被刷新并清理
  • 响应头 — HTTP 状态码和响应头会重置为默认值
  • 错误状态 — 最后一次的错误信息(消息、文件、行号、类型)和连接状态会被清除。用户注册的错误处理器(set_error_handler())、异常处理器(set_exception_handler())以及 error_reporting() 级别会在请求之间持续存在
在请求之间持续存在
  • 外层作用域中的变量 — 在 oxphp_worker() 之前定义并通过 use 捕获的任何内容
  • 静态属性 — 类的静态属性会保留其值
  • 数据库连接 — PDO、MySQLi 和其他持久连接会保持打开
  • 自动加载器 — 已注册的自动加载器(Composer、自定义)会保持活动状态
  • 已加载的类和函数 — 所有之前加载的类、接口、trait 和函数

回收

当满足以下任一条件时,工作进程会被自动回收(以全新的 PHP 进程重启):

  • 超出最大内存 — 工作进程的 PHP 内存使用量超过 WORKER_MAX_MEMORY_MIB MiB
  • 应用请求退出 — 处理函数调用了 Worker::scheduleExit()。这对于应用控制的热重载、基于文件 mtime 的重载或每个请求的引导重新执行很有用
  • 连续错误 — 工作进程遇到 3 次连续的处理函数失败(致命错误、超时或未处理的异常)。注意,exit()/die() 调用不计为失败

当工作进程被回收时,PHP 进程会终止并启动一个新进程,重新执行工作进程脚本的外层作用域。对于基于内存和计划退出的回收,当前请求会正常完成,然后工作进程才退出。对于基于错误的回收,工作进程会在失败的请求之后退出。

开发时重载

工作进程模式会在内存中持续保存引导状态(自动加载器、DI 容器、数据库连接),因此仅靠 opcache.validate_timestamps=1 不足以让在外层作用域运行的代码的改动生效。对于开发循环,有两种选择:

  • 每个请求都回收。 在每次处理函数调用结束时调用 OxPHP\Server\Worker::current()->scheduleExit()(例如,用一个 OXPHP_DEV 环境变量标志来控制)。当前请求会正常完成,然后工作进程退出并重新生成,重新执行外层作用域。这以牺牲工作进程模式的性能优势为代价,换取了 FPM 风格的重载语义。对于活跃的开发工作,这是最简单也最可靠的方法。
  • 让工作进程保持热态,重载请求处理函数。 完全跳过 scheduleExit(),启用 opcache.validate_timestamps=1,并让你的引导保持精简。在请求回调内部加载的代码会在下一个请求时被 OPcache 刷新;在外层作用域加载一次的代码则不会。完整的注意事项列表请参见 OPcache 与 JIT → 开发设置

故障排查

请求挂起,永不完成

如果引导脚本中从未调用 oxphp_worker(),则不会分发任何请求,每个请求都会无限期等待。请确认你的脚本在正常代码路径中无条件地调用了 oxphp_worker()

状态在请求之间泄漏

oxphp_worker() 回调内部定义的变量会被 PHP 的垃圾回收器清理,但在外层作用域定义的静态属性和全局变量会持续存在。如果你看到一个请求的数据出现在另一个请求中,请检查是否有静态属性或全局变量在多次调用之间累积状态。

修复方法: 在每个请求回调开始时显式重置静态状态,或者避免在静态变量中存储每个请求的状态。

工作进程立即回收(内存限制)

工作进程的内存限制会在每个请求之后使用 PHP 报告的内存使用量进行检查。如果你的引导阶段分配了大量内存(例如加载一个大缓存),初始内存占用可能已经接近限制。

修复方法: 提高 WORKER_MAX_MEMORY_MIB,或将大块分配推迟到第一个请求。

工作进程立即回收(错误限制)

三次连续的处理函数失败会触发回收。请检查你的应用日志,查找请求回调中发生的异常或致命错误。

检查: 在访问日志或结构化日志输出中查找错误:

bash
docker logs <container> 2>&1 | grep '"level":"error"'
空闲后数据库连接断开

如果你的数据库服务器会关闭空闲连接,那么下一个请求中的重连尝试可能会失败。请使用能处理重连的连接池,或者捕获异常并手动重连。

Docker 示例

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.0 ports: - "8080:80" volumes: - ./src:/var/www/html environment: - DOCUMENT_ROOT=/var/www/html/public - WORKER_MODE_ENABLED=true - ENTRY_FILE=/var/www/html/worker.php - WORKER_MAX_MEMORY_MIB=128

PHP API

工作进程的自省能力和工作进程入口点通过 OxPHP\Server\Worker 类暴露。

php
<?php $worker = OxPHP\Server\Worker::current(); $worker->serve(function () { handleRequest(); });

遗留自由函数oxphp_is_workeroxphp_worker_idoxphp_worker)仍然可用,并通过相同的内部状态进行路由。新代码应优先使用类 API。

该类还暴露了对优雅自我回收、可观测性和健康检查有用的运行时自省能力:

方法 返回值
Worker::isWorkerMode(): bool 服务器是否运行在工作进程模式
$worker->id(): int 稳定的每线程工作进程 ID
$worker->startTime(): float 该工作进程启动时的 Unix 时间戳
$worker->requestCount(): int 该工作进程已处理的请求数
$worker->memoryUsage(): int 该工作进程当前的 memory_get_usage(true)
$worker->rss(): int 当前的常驻集大小(字节,Linux/macOS)
$worker->maxMemoryBytes(): int 回收阈值 — WORKER_MAX_MEMORY_MIB × 1 MiB,无限制时为 0
$worker->isExitScheduled(): bool 是否已调用 scheduleExit()
$worker->exitReason(): ?string 运行时为 null;工作进程正在关闭时为 "scheduled""max_memory""error"

完整的签名和实例请参见 OxPHP\Server\Worker

PHP 示例

检测工作进程模式

使用 OxPHP\Server\Worker::isWorkerMode() 检查当前进程是否运行在工作进程模式。这对于编写在传统模式和工作进程模式下都能工作的代码很有用。

php
<?php if (OxPHP\Server\Worker::isWorkerMode()) { // Reuse a persistent connection $redis = new Redis(); $redis->pconnect('redis', 6379); } else { // Traditional mode: connect per request $redis = new Redis(); $redis->connect('redis', 6379); }

Symfony 工作进程脚本

worker.php
<?php use App\Kernel; require __DIR__ . '/../vendor/autoload.php'; $kernel = new Kernel('prod', false); $kernel->boot(); oxphp_worker(function () use ($kernel) { $request = Symfony\Component\HttpFoundation\Request::createFromGlobals(); $response = $kernel->handle($request); $response->send(); $kernel->terminate($request, $response); }); $kernel->shutdown();

最佳实践

  • 设置 WORKER_MAX_MEMORY_MIB(例如 128),这样发生泄漏的工作进程就会自动回收,而不是耗尽主机资源。可再结合 Worker::scheduleExit() 实现由应用驱动的回收。
  • 避免在静态属性或全局变量中存储每个请求的状态。 由于这些内容会在请求之间持续存在,一个请求残留的状态可能会泄漏到另一个请求中。
  • 尽早验证软重置。 在开发标志下向你的处理函数添加 Worker::current()->scheduleExit(),并对应用进行端到端的演练。这能在你投入使用长期存活的工作进程之前捕获状态泄漏的 bug。
  • 处理数据库空闲超时。 如果你的数据库驱动在空闲一段时间后会断开连接,请捕获异常并重连,或者使用能自动处理重连的连接池。
  • 保持外层作用域精简。 只引导真正需要持续存在的内容:自动加载器、配置和共享服务。将请求相关的设置推迟到回调中。

另请参阅

  • 路由 — 工作进程模式如何接入 URL 路由
  • 提前响应 — 立即发送响应并继续后台处理
  • PHP 函数oxphp_worker()oxphp_is_worker() 及其他内置函数的完整参考
  • 配置参考 — 环境变量的完整列表