FrankenPHP Worker 模式完全指南:一次启动、常驻内存的 PHP 应用服务器
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
导读
本文以 FrankenPHP 官方文档(docs/it/worker.md)为主体,系统讲解 Worker 模式的启动方式、自定义 Worker 编写、崩溃恢复、超全局变量行为与状态持久化等核心机制,并辅以仓库源码(worker.go、threadworker.go、caddy/workerconfig.go、caddy/admin.go)作为实现依据。读完本文,你将掌握如何把 PHP 应用一次性加载进内存、在毫秒级响应请求,并能在生产环境中正确配置、监控与重启 Worker。
一、Worker 模式是什么
传统 PHP 的每个请求都会重新加载应用、重新解析代码。Worker 模式则相反:应用只启动一次,随后常驻内存,由 FrankenPHP 负责把到达的 HTTP 请求快速分发到已就绪的 Worker 进程/线程上处理,从而把每请求的开销降到最低。
从源码看,worker.go中每个worker结构体代表"一个 Worker 脚本",可以挂载多个线程:
// worker.go type worker struct { mercureContext name string fileName string num int maxThreads int ... maxConsecutiveFailures int ... }Worker 脚本通过 C 扩展暴露的frankenphp_handle_request()函数接收请求并返回响应,每次调用会阻塞直到下一个请求到来。
二、启动 Worker
2.1 使用 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/frankenphpFRANKENPHP_CONFIG中的路径是容器内路径,因此示例中与-v $PWD:/app的挂载点保持一致。
2.2 使用独立二进制启动
使用php-server命令的--worker选项,即可用 Worker 模式伺服当前目录内容:
frankenphp php-server --worker /path/to/your/worker/script.php如果 PHP 应用已嵌入到二进制文件中,可以在应用根目录添加自定义Caddyfile,它会被自动采用,无需额外传参。
2.3 监听文件变化自动重启(--watch)
Worker 常驻内存后,代码修改不会自动生效。可以用--watch选项在文件变化时自动重启 Worker,详见 config.md 的文件变化控制。
下面的命令会在/path/to/your/app/及其子目录中任何.php文件被修改时触发重启:
frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php"--watch经常与热重载(Hot Reload)搭配使用:前者负责重启 Worker,后者负责刷新浏览器页面,实现接近"保存即生效"的开发体验。
三、框架集成
3.1 Symfony
Symfony 用户请直接参考 Symfony Worker 模式文档,其中包含与 FrankenPHP 结合的具体配置与命令。
3.2 Laravel Octane
Laravel 用户可参考 Laravel Octane 文档,FrankenPHP 是 Octane 官方支持的服务器之一。
四、编写自定义 Worker
不依赖任何第三方库,用纯 PHP 就能写出自己的 Worker。下面是官方文档提供的完整示例:
<?php // public/index.php // Avvio dell'applicazione require __DIR__.'/vendor/autoload.php'; $myApp = new \App\Kernel(); $myApp->boot(); // Gestore fuori dal ciclo per migliori prestazioni (meno lavoro per richiesta) $handler = static function () use ($myApp) { try { // Chiamato quando arriva una richiesta, // i superglobali, php://input e simili vengono reimpostati echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER); } catch (\Throwable $exception) { // `set_exception_handler` viene chiamato solo quando termina lo script worker, // il che potrebbe non essere il comportamento atteso, quindi intercettare e gestire qui le eccezioni (new \MyCustomExceptionHandler)->handleException($exception); } }; $maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { $keepRunning = \frankenphp_handle_request($handler); // Esegui qualcosa dopo aver inviato la risposta HTTP $myApp->terminate(); // Richiama il garbage collector per ridurre la probabilità che parta durante la generazione di una pagina gc_collect_cycles(); if (!$keepRunning) break; } // Pulizia $myApp->shutdown();代码要点:
- 启动逻辑放循环外:
require autoload、boot()等昂贵操作只执行一次,这是性能提升的关键; $handler是静态闭包:每个请求只调用闭包本身,避免每次重建;- 异常必须自行捕获:文档特别指出,
set_exception_handler只有在 Worker 脚本退出时才会触发,与直觉不符,所以要在循环内拦截并处理; gc_collect_cycles()手动回收:在响应发送后、等待下一个请求前主动触发,降低页面生成过程中 GC 介入的概率;$keepRunning返回值:frankenphp_handle_request()返回 false 时退出循环,进行优雅清理(shutdown())。
随后用FRANKENPHP_CONFIG环境变量指向该脚本启动:
docker run \ -e FRANKENPHP_CONFIG="worker ./public/index.php" \ -v $PWD:/app \ -p 80:80 -p 443:443 -p 443:443/udp \ dunglas/frankenphp4.1 Worker 数量:默认 2 个/CPU,可显式指定
默认情况下,每个 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对应到源码,caddy/workerconfig.go中的worker指令还支持更细粒度的 Caddyfile 配置项:
| 子指令 | 说明 |
|---|---|
name | Worker 名称,默认取文件名 |
file | Worker 脚本路径(必填) |
num | 启动的 Worker 数量 |
max_threads | 该 Worker 允许的最大线程数,超过后不再扩容(见 scaling.go 的扩容逻辑) |
env | 为 Worker 注入额外环境变量,可重复指定 |
watch | 监听的文件/目录模式,留空使用默认模式 |
match | 按请求路径匹配该 Worker |
max_consecutive_failures | 最大连续失败次数(详见下文) |
4.2 每处理 N 个请求后自动重启(MAX_REQUESTS)
PHP 语言本身并非为常驻进程设计,大量旧库和遗留代码存在内存泄漏。在 Worker 模式下应对这类代码的通用做法是:处理完一定数量的请求后主动重启 Worker。
上面的示例代码已经内置了该能力——通过环境变量MAX_REQUESTS设置上限:
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {当MAX_REQUESTS为 0(默认)时不限制;设置为 N 时,Worker 处理完 N 个请求后正常退出,由 FrankenPHP 拉起新的 Worker 进程,从而周期性回收内存。
五、手动重启 Worker(Caddy Admin API)
除了按文件变化自动重启之外,还可以通过 Caddy 的管理 API 中启用了 Admin 端点。
发送一条简单的 POST 请求即可:
curl -X POST http://localhost:2019/frankenphp/workers/restart该端点定义在 caddy/admin.go 中:
// EXPERIMENTAL: These routes are not yet stable and may change in the future. func (admin FrankenPHPAdmin) Routes() []caddy.AdminRoute { return []caddy.AdminRoute{ { Pattern: "/frankenphp/workers/restart", Handler: caddy.AdminHandlerFunc(admin.restartWorkers), }, { Pattern: "/frankenphp/threads", Handler: caddy.AdminHandlerFunc(admin.threads), }, } }注意三点:
- 该路由仅接受 POST,其他方法返回 405;成功时响应
"workers restarted successfully\n",对应测试见 caddy/admin_test.go; - 源码注释明确标注EXPERIMENTAL(实验性),路由在未来版本可能变化;
- 同模块还提供
/frankenphp/threads端点,可查看各线程的运行状态快照。
底层实现调用frankenphp.RestartWorkers()(见 worker.go),它会等待所有 Worker 线程让出控制权后统一重启——源码注释强调"所有 Worker 必须同时重启,以避免 opcache 重置带来的不一致问题"。
六、Worker 崩溃处理与指数退避
如果 Worker 以非零退出码崩溃,FrankenPHP 会按**指数退避(exponential backoff)**策略重启它。判断逻辑如下:
- 若 Worker 存活时间超过"上一次退避时长 × 2",则不被惩罚,直接再次重启;
- 若 Worker 在短时间内持续以非零退出码失败(例如脚本存在拼写错误),FrankenPHP 最终会以错误
too many consecutive failures停止。
源码实现位于 threadworker.go:
// wait a bit and try again (exponential backoff) backoffDuration := time.Duration(handler.failureCount*handler.failureCount*100) * time.Millisecond if backoffDuration > time.Second { backoffDuration = time.Second } handler.failureCount++ time.Sleep(backoffDuration)即退避时长按失败次数的平方增长:1 次失败约 100ms、2 次约 400ms、3 次约 900ms……封顶 1 秒。触发终止的判定:
if worker.maxConsecutiveFailures >= 0 && startupFailChan != nil && !watcherIsEnabled && handler.failureCount >= worker.maxConsecutiveFailures { startupFailChan <- fmt.Errorf("too many consecutive failures: worker %s has not reached frankenphp_handle_request()", worker.fileName) handler.thread.state.Set(state.ShuttingDown) return }6.1 配置 max_consecutive_failures
最大连续失败次数可在 Caddyfile 的frankenphp块中配置:
frankenphp { worker { # ... max_consecutive_failures 10 } }依据 caddy/workerconfig.go:
- 默认值为 6;
- 设为-1 表示永不因连续失败而终止(
set to -1 to never panick); - 取值必须
>= -1,否则 Caddyfile 解析直接报错(max_consecutive_failures must be >= -1); - 当
--watch监听模式开启时,该上限判定会被跳过(!watcherIsEnabled),因为脚本失败更可能是文件改动所致。
七、超全局变量(Superglobals)的行为
PHP 超全局变量($_SERVER、$_ENV、$_GET等)在 Worker 模式下遵循以下规则:
- 在第一次调用
frankenphp_handle_request()之前,超全局变量保存的是Worker 脚本自身的值; - 在
frankenphp_handle_request()调用期间及之后,超全局变量保存的是当前 HTTP 请求的值;每次调用都会用新请求的值覆盖它们。
如果要在回调内访问 Worker 脚本自己的超全局变量,必须在第一次调用前复制,并导入闭包作用域:
<?php // Copia il superglobale $_SERVER del worker prima della prima chiamata a frankenphp_handle_request() $workerServer = $_SERVER; $handler = static function () use ($workerServer) { var_dump($_SERVER); // $_SERVER legato alla richiesta var_dump($workerServer); // $_SERVER dello script worker }; // ...7.1 重要差异:$_ENV 不会被重置
大部分超全局变量($_GET、$_POST、$_COOKIE、$_FILES、$_SERVER、$_REQUEST)会在请求之间自动重置。
但$_ENV目前不会在请求之间重置。这意味着:
- 某次请求中对
$_ENV的任何修改,都会持续存在,并对同一 Worker 线程后续处理的请求可见; - 因此不要在
$_ENV中存放敏感数据或请求专属数据。
这是当前实现的行为约束(原文档明确标注"attualmente"即"目前"),未来版本可能改变,写 Worker 时请务必留意。
八、状态持久化:Worker 模式的性能之源与副作用之源
由于 Worker 模式让 PHP 进程在请求之间保持存活,以下状态会跨请求持续存在:
- 静态变量:函数/方法内以
static声明的变量,其值跨请求保留; - 类静态属性:类上的
static属性跨请求保留; - 全局变量:Worker 全局作用域中的变量跨请求保留;
- 内存缓存:请求处理器外部存储于内存中的任何数据(数组、对象)都会保留。
这是设计使然,也正是 Worker 模式快的原因——但要警惕副作用。官方文档给出了经典反例:
<?php function getCounter(): int { static $count = 0; return ++$count; // Incrementa tra le richieste! } $handler = static function () { echo getCounter(); // 1, 2, 3, ... per ogni richiesta su questo thread }; while (\frankenphp_handle_request($handler)) { // ... }这段代码会按请求依次输出 1、2、3……因为$count在请求间持续递增。
编写 Worker 时,务必在每个请求之间重置一切请求专属状态。Symfony 与 Laravel Octane 等框架会自动恢复大部分状态,但某些服务仍可能需要手动重置:
- Symfony 中,持有请求专属状态的服务应实现
Symfony\Contracts\Service\ResetInterface,以便内核在请求之间调用其reset()方法。
九、进一步阅读
- 官方 Worker 文档原文:docs/it/worker.md(另有 docs/worker.md 等语言版本)
- 文件监听与 Caddyfile 配置:docs/it/config.md
- 热重载:docs/it/hot-reload.md
- Symfony 集成:docs/it/symfony.md
- Laravel Octane 集成:docs/it/laravel.md
- Worker 核心实现:worker.go、threadworker.go
- Caddyfile
worker指令解析:caddy/workerconfig.go - 管理 API 重启端点:caddy/admin.go 及其测试 caddy/admin_test.go
- 指标(
frankenphp_ready_workers等)可辅助观测 Worker 就绪状态:metrics.go
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考