FrankenPHP Worker 模式的原理与运行模型深度解析(2026 年最新官方文档 + 源码行为)
FrankenPHP 是 Go(Caddy)+ 嵌入式 PHP(ZTS 线程安全版) 的现代 PHP 应用服务器。Worker 模式 是其性能杀手锏:应用只启动一次,常驻内存,每个请求只需几毫秒即可处理,绕过传统 PHP-FPM/经典模式每次请求的完整引导(autoload + Kernel boot + 服务容器构建)。
1. 整体架构与线程模型(核心原理)
- FrankenPHP 单进程多线程 模型(非多进程)。
- Caddy(Go)负责 HTTP/3、TLS、路由、静态文件等。
- PHP 解释器以 ZTS(Zend Thread Safety) 形式嵌入,使用 Go 的
cgo+ PHP embed API。 - Worker 池:默认启动 2 × CPU 核心数 的 PHP 线程(可通过
num_threads/worker.num配置)。 - 每个线程独立运行一个 PHP 上下文(类似每个线程一个 mini-PHP-FPM),但共享同一个进程的内存(OPcache、APCu、扩展状态等完全共享)。
- 并发机制:
- 每个 PHP 线程 一次只能处理 1 个请求(顺序执行)。
- Go goroutine + Caddy 负责把请求 调度 到空闲的 PHP 线程。
- 当所有线程都忙时,可动态扩容线程(
max_threads),上限由内存自动估算或手动设置。
2. Worker 脚本的运行生命周期(最核心模型)
Worker 脚本(通常是public/index.php 或自定义 worker.php)的执行流程如下:<?php
// 1. 启动阶段(只执行一次)
require __DIR__.'/vendor/autoload.php';
$myApp = new \App\Kernel();
$myApp->boot(); // ← 框架、容器、DB 连接、Redis 等全部初始化
// 2. 定义请求处理器(闭包,复用对象)
$handler = static function () use ($myApp) {
// 这里 superglobals、php://input 已重置为当前请求
try {
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
} catch (\Throwable $e) {
// 异常必须在这里捕获(set_exception_handler 在 worker 结束时才生效)
}
};
// 3. 无限循环(事件循环)
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler); // ← 关键阻塞调用
// 请求响应已发送后执行清理
$myApp->terminate(); // Laravel/Symfony 提供的重置方法
gc_collect_cycles(); // 主动 GC,防止请求中途触发
if (!$keepRunning) break; // Caddy 优雅关闭时返回 false
}
// 4. 清理阶段(worker 重启/关闭时执行)
$myApp->shutdown();
详细生命周期图解(官方 + 社区描述):
1. Caddy 启动 → 为每个 worker 创建一个 PHP 线程 → 执行 worker 脚本。
2. 脚本运行到 \frankenphp_handle_request() → PHP 解释器暂停,线程进入等待状态(类似 select())。
3. HTTP 请求到达 Caddy → Caddy 把请求上下文(headers、body、$_SERVER 等)注入 当前空闲的 PHP 线程。
4. \frankenphp_handle_request() 唤醒线程 → 重置 superglobals → 调用 $handler 闭包。
5. 闭包内执行 PHP 业务代码 → echo / header() 等输出 → 响应通过 Caddy 直接发送给客户端。
6. 闭包返回 → 线程继续执行循环体(terminate + GC)→ 再次阻塞在下一轮 frankenphp_handle_request()。
关键点:应用启动代码只执行一次,后续请求直接跳到 handler。
3. 关键函数:\frankenphp_handle_request()
- 这是一个 FrankenPHP 内置的 PHP 扩展函数(由
frankenphp.c+ Go 侧 Workers 接口实现)。 - 作用:阻塞等待 + 上下文切换。
- Go 侧通过 channel/队列 把请求推送给 PHP 线程。
- PHP 侧接收后,原子级替换 当前请求的 superglobals、$_FILES、php://input 等。
- 返回值:
true:继续循环(正常请求)。false:Caddy 正在优雅关闭,退出循环。
4. 超全局变量(Superglobals)行为(最容易踩坑)
- 启动时(第一次
frankenphp_handle_request()前):$_SERVER等是 CLI 风格(SCRIPT_FILENAME 为 worker 脚本本身)。 - 每次请求中(回调内部):被完全替换为当前 HTTP 请求的值。
- 回调结束后:保持最后一次请求的值,直到下次请求覆盖。
// 在循环前复制初始值
$workerServer = $_SERVER;
$handler = static function () use ($workerServer) {
// $_SERVER 是本次请求的
// 要用初始值时:$workerServer
};
5. 配置参数对运行模型的影响
frankenphp {
num_threads 8 # 全局 PHP 线程数(默认 2×CPU)
max_threads auto # 动态扩容上限
worker {
file /app/public/index.php
num 12 # 该 worker 专用的线程数
max_consecutive_failures 10
watch **/*.php # 热重载
}
}
6. 与经典模式(非 Worker)的对比
| 项目 | 经典模式(php-server) | Worker 模式 |
|---|---|---|
| 应用启动 | 每次请求都完整启动 | 只启动一次,常驻内存 |
| 性能 | 比 PHP-FPM 快 30-50% | 再快 3-10 倍(框架启动开销归零) |
| 内存模型 | 每个请求独立进程/执行 | 多线程共享进程,状态持久 |
| 适用场景 | 任意传统 PHP 应用 | 需要修改 worker 脚本(Laravel Octane、Symfony Runtime 已原生支持) |
| 内存泄漏风险 | 低 | 高(需主动 terminate + GC) |
7. 最佳实践 & 注意事项
- 必须在
$handler内创建请求级对象(Request、DB 查询等)。 - 必须调用框架的
terminate()/reset()方法(Laravel Octane、Symfony Messenger 已做好)。 - 内存泄漏:定期
gc_collect_cycles()+MAX_REQUESTS=1000强制重启 worker。 - 数据库连接:用 PDO::ATTR_PERSISTENT 或在 worker 启动时建立长连接。
- Xdebug / Blackfire:需要特殊 middleware 包裹 handler。
- 热重载:开发时加
--watch或worker.watch,生产用curl -X POST /frankenphp/workers/restart。
frankenphp_handle_request 这个精巧的上下文切换机制,实现“启动一次、请求永驻”的极致性能。官方文档地址(强烈推荐阅读原文):
- https://frankenphp.dev/docs/worker/
- https://frankenphp.dev/docs/performance/