FrankenPHP Worker 模式实战指南:让 PHP 应用常驻内存,毫秒级处理请求
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
导读
本篇指南完整讲解 FrankenPHP 的 Worker 模式:它只启动一次 PHP 应用程序并将框架、连接池与编译产物保存在内存中,此后每个传入请求都由常驻进程在几毫秒内直接处理,彻底消除传统 PHP-FPM 模型下"每个请求重新引导框架"的开销。读完本文,你将掌握如何通过 Docker 与独立二进制文件启动 worker、如何编写自己的 worker 脚本、如何处理超全局变量与状态持久化,以及如何在请求数超限、文件变更或进程崩溃时优雅地重启 worker。
什么是 FrankenPHP Worker 模式
FrankenPHP 基于 Caddy 构建,内置 PHP 解释器。在常规模式下,每个请求都会重新加载并执行应用代码;而在 Worker 模式下,worker脚本在进程启动时执行一次,随后进入一个由frankenphp_handle_request()驱动的循环,反复接收并处理请求,中间不再重新引导应用。官方对它的定位是:
启动一次应用程序并将其保存在内存中,FrankenPHP 将在几毫秒内处理传入请求。
从源码结构看,frankenphp_handle_request是这一模式的核心入口,其 PHP 侧签名定义在 frankenphp.stub.php 中:
function frankenphp_handle_request(callable $callback): bool {}它接收一个回调,返回bool表示是否继续运行。每次调用该函数,PHP 线程都会等待并取出一个 HTTP 请求,执行回调处理后回到循环等待下一个请求。
启动 worker 脚本
使用 Docker 启动
将FRANKENPHP_CONFIG环境变量的值设置为worker /path/to/your/worker/script.php即可:
docker run \ -e FRANKENPHP_CONFIG="worker /app/path/to/your/worker/script.php" \ -v $PWD:/app \ -p 80:80 -p 443:443 -p 443:443/udp \ dunglas/frankenphp这里的FRANKENPHP_CONFIG不是魔法字符串:Docker 镜像内置的 caddy/frankenphp/Caddyfile 通过{$FRANKENPHP_CONFIG}占位符把该环境变量的内容直接注入全局frankenphp {}配置块,从而在启动时注册 worker。
使用独立二进制文件启动
使用php-server命令的--worker选项,即可让 worker 为当前目录的内容提供服务:
frankenphp php-server --worker /path/to/your/worker/script.php如果你的 PHP 应用程序已嵌入到二进制文件中,可以在应用的根目录添加自定义Caddyfile,它会被自动使用。
文件变更时自动重启 worker
使用--watch选项可以在文件更改时重启 worker。下面这条命令会在/path/to/your/app/目录或其子目录中任何以.php结尾的文件被修改时触发重启:
frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php"该功能通常与热重载结合使用,实现"改完代码立刻生效"的开发体验。从实现看,--watch在配置层面对应worker指令中的watch子指令,支持传入多个 glob 模式(见 caddy/workerconfig.go 的unmarshalWorker解析逻辑)。
Symfony Runtime 与 Laravel Octane 支持
Symfony Runtime(Symfony 7.4 之前)
[!TIP] 以下部分仅在 Symfony 7.4 之前是必需的,因为 Symfony 7.4 引入了对 FrankenPHP worker 模式的原生支持。
FrankenPHP 的 worker 模式由 Symfony Runtime 组件支持。要在 worker 中启动任何 Symfony 应用程序,请安装 FrankenPHP 对应的 Runtime 包:
composer require runtime/frankenphp-symfony然后通过定义APP_RUNTIME环境变量,使用 FrankenPHP Symfony Runtime 启动应用服务器:
docker run \ -e FRANKENPHP_CONFIG="worker ./public/index.php" \ -e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \ -v $PWD:/app \ -p 80:80 -p 443:443 -p 443:443/udp \ dunglas/frankenphpLaravel Octane
Laravel 应用请参阅专门的文档,其中详细说明了如何将 Octane 的运行模式切换为 FrankenPHP worker,并获得与 Octane 生态完全兼容的体验。
编写自定义 worker 脚本
不依赖任何第三方库,你也可以自己编写 worker 脚本。官方示例展示了完整的写法:
<?php // public/index.php // 启动你的应用程序 require __DIR__.'/vendor/autoload.php'; $myApp = new \App\Kernel(); $myApp->boot(); // 在循环外的处理器以获得更好的性能(减少工作量) $handler = static function () use ($myApp) { try { // 当收到请求时调用, // 超全局变量、php://input 等都会被重置 echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER); } catch (\Throwable $exception) { // `set_exception_handler` 仅在 worker 脚本结束时调用, // 这可能不是您所期望的,因此在此处捕获并处理异常 (new \MyCustomExceptionHandler)->handleException($exception); } }; $maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { $keepRunning = \frankenphp_handle_request($handler); // 在发送 HTTP 响应后做一些事情 $myApp->terminate(); // 调用垃圾收集器以减少在页面生成过程中触发垃圾收集的可能性 gc_collect_cycles(); if (!$keepRunning) break; } // 清理 $myApp->shutdown();要点解析:
- 把
$handler定义在循环外:请求处理逻辑只创建一次闭包,循环内只做调用,把每请求的工作量降到最低。 - 异常必须在回调内捕获:
set_exception_handler只会在 worker 脚本结束时被调用,如果不在回调里捕获,一次请求中的异常可能直接终止整个常驻进程。 gc_collect_cycles()主动触发垃圾回收:避免垃圾回收在页面生成的中间时刻被随机触发,保证请求响应的稳定性。$keepRunning返回值:当连接中断或服务器准备关闭时,frankenphp_handle_request()返回false,循环随之退出并执行清理。
随后,启动应用并通过FRANKENPHP_CONFIG环境变量配置 worker:
docker run \ -e FRANKENPHP_CONFIG="worker ./public/index.php" \ -v $PWD:/app \ -p 80:80 -p 443:443 -p 443:443/udp \ dunglas/frankenphp控制 worker 数量
默认情况下,每个 CPU 启动 2 个 worker。你也可以显式指定要启动的 worker 数量,只需在脚本路径后追加一个数字参数:
docker run \ -e FRANKENPHP_CONFIG="worker ./public/index.php 42" \ -v $PWD:/app \ -p 80:80 -p 443:443 -p 443:443/udp \ dunglas/frankenphp这个数量对应源码中的worker.num字段:在 worker.go 的initWorkers中,FrankenPHP 会为每个 worker 启动num个 PHP 线程(convertToWorkerThread),并把它们全部加入就绪等待组,全部就绪后服务才开始对外提供请求。
在处理一定数量的请求后重启 worker
由于 PHP 最初不是为长时间运行的进程而设计的,仍有许多库和传统代码会泄漏内存。在 worker 模式下使用此类代码的一个解决方法,是在处理一定数量的请求后重启 worker 脚本。
前面的 worker 代码片段允许通过设置名为MAX_REQUESTS的环境变量来配置要处理的最大请求数。当循环计数达到该值时,脚本自然结束,FrankenPHP 会以退出码 0 重新拉起脚本,完成一次干净的内存回收。
这一机制在更底层还有对应的全局配置:frankenphp指令支持max_requests选项(见 caddy/app.go 中的MaxRequests字段,0 表示不限制),由threadworker.go中waitForWorkerRequest的maxRequestsPerThread判断触发线程重启,实现完整的 ZTS 清理。仓库测试 worker_test.go 中的TestWorkerMaxRequests验证了:设置max_requests=5时,单个 worker 实例处理的请求数绝不会超过 5,且日志中会出现max requests reached, restarting。
手动重启所有 workers
除了文件更改时自动重启外,还可以通过 Caddy admin API 优雅地重启所有 workers。只要在 Caddyfile 中启用了 admin,用一个简单的 POST 请求即可触发重启:
curl -X POST http://localhost:2019/frankenphp/workers/restart该端点在 caddy/admin.go 中注册(/frankenphp/workers/restart),只接受POST方法,内部调用frankenphp.RestartWorkers()。从 worker.go 的实现看,RestartWorkers会同时向所有 worker 线程发出重启信号,确保所有 worker 在同一时刻重启,以避免 opcache 重置带来的不一致问题。同模块还提供了/frankenphp/threads端点,可返回当前线程的调试状态。
Worker 故障与指数退避
如果 worker 脚本因非零退出代码而崩溃,FrankenPHP 将使用指数退避策略重启它。如果 worker 脚本保持运行的时间超过上次退避 × 2,它将不会惩罚 worker 脚本并再次重启它。但是,如果 worker 脚本在短时间内继续以非零退出代码失败(例如,脚本中有拼写错误),FrankenPHP 将崩溃并出现错误:too many consecutive failures。
从 threadworker.go 的实现可以看到退避的具体计算:backoffDuration = failureCount × failureCount × 100ms,上限 1 秒;当脚本尚未到达frankenphp_handle_request()且连续失败次数超过阈值时,会向启动失败通道写入too many consecutive failures错误并关闭线程。
可以在 Caddyfile 中使用max_consecutive_failures选项配置连续失败的次数(默认值为 6,设为 -1 则永不触发崩溃保护):
frankenphp { worker { # ... max_consecutive_failures 10 } }Worker 指令的完整配置面
除了上面的max_consecutive_failures,worker块在 caddy/workerconfig.go 中还支持以下子指令:
| 子指令 | 说明 |
|---|---|
name | worker 的名称,用于指标与日志标识;默认使用脚本文件名,模块 worker 默认以m#前缀命名 |
file | worker 脚本路径(必填) |
num | 启动的 worker 数量,默认每个 CPU 2 个 |
max_threads | 该 worker 允许的最大线程数,达到上限后不再自动扩容 |
env | 为 worker 注入额外环境变量,可重复指定多个 |
watch | 监听文件变更的 glob 模式,不传参数时使用默认模式 |
match | 请求路径匹配规则,用于把特定 URL 路由到该 worker |
max_consecutive_failures | 连续启动失败上限,默认 6,-1 表示永不触发 |
FRANKENPHP_CONFIG中的worker ./public/index.php 42语法本质上是上述配置的简写形式:第一个参数是file,第二个数字是num。
超全局变量行为
PHP 超全局变量($_SERVER、$_ENV、$_GET...)在 worker 模式下遵循如下规则:
- 在第一次调用
frankenphp_handle_request()之前,超全局变量包含绑定到 worker 脚本本身的值(例如启动脚本时注入的环境变量)。 - 在调用
frankenphp_handle_request()期间和之后,超全局变量包含从处理的 HTTP 请求生成的值,每次调用frankenphp_handle_request()都会更改超全局变量的值。
要在回调内访问 worker 脚本自身的超全局变量,必须先将它们复制出来,并把副本导入回调的作用域:
<?php // 在第一次调用 frankenphp_handle_request() 之前复制 worker 的 $_SERVER 超全局变量 $workerServer = $_SERVER; $handler = static function () use ($workerServer) { var_dump($_SERVER); // 与请求绑定的 $_SERVER var_dump($workerServer); // worker 脚本的 $_SERVER }; // ...大多数超全局变量($_GET、$_POST、$_COOKIE、$_FILES、$_SERVER、$_REQUEST)会在请求之间自动重置,但$_ENV目前不会在请求之间重置。这意味着在某个请求中对$_ENV的修改会持续保留,并被同一 worker 线程处理的后续请求看到。因此应避免在$_ENV中存放与请求相关或敏感的数据。
状态持久化与注意事项
由于 worker 模式让 PHP 进程在请求之间保持存活,以下状态会跨请求持续存在(这正是 worker 模式速度快的根本原因):
- 静态变量:函数或方法内使用
static声明的变量会保留其值。 - 类静态属性:类上的静态属性会跨请求持久化。
- 全局变量:worker 脚本全局作用域中的变量会跨请求持久化。
- 内存缓存:请求处理器之外保存在内存中的数据(数组、对象)都会持续存在。
这种设计是刻意的,但需要小心避免意外的副作用。例如下面这段代码,static $count会跨请求递增:
<?php function getCounter(): int { static $count = 0; return ++$count; // 跨请求递增! } $handler = static function () { echo getCounter(); // 每个请求依次输出 1, 2, 3, ... }; while (\frankenphp_handle_request($handler)) { // ... }编写 worker 脚本时,务必在请求之间重置任何与请求相关的状态。Symfony 与 Laravel Octane 等框架会替你重置大部分状态,但你可能仍需要重置自己编写的服务。例如在 Symfony 中,持有请求相关状态的服务应实现ResetInterface,以便内核在请求之间统一重置。
小结与更多资料
Worker 模式是 FrankenPHP 实现高性能的关键:一次引导、常驻内存、循环处理,配合MAX_REQUESTS上限、--watch文件监听、admin API 手动重启与指数退避故障恢复,可以在享受常驻进程性能的同时有效规避 PHP 长驻进程的经典陷阱(内存泄漏、状态污染、启动失败)。
继续深入可阅读仓库中的相关资源:
- 配置参考:docs/config.md、caddy/workerconfig.go
- 核心实现:worker.go、threadworker.go
- 管理端点:caddy/admin.go
- 测试用例:worker_test.go(覆盖
MAX_REQUESTS、崩溃日志、环境变量等行为) - 相关生态:Laravel Octane、热重载、嵌入二进制
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考