将现有的 PHP 网站(通常运行在 PHP-FPM 这种“用完即焚”的模式下)改造成 FrankenPHP 的 Worker 模式(常驻进程模式),可以显著提升性能(因为只需要加载一次框架/应用引导代码)。但这也带来了内存管理和状态隔离的挑战。
以下是完整的改造方案,分为基础设施层、入口文件层和代码逻辑层三个部分。
核心概念区别
PHP-FPM: 每个请求都是独立的。请求开始 -> 加载文件 -> 初始化变量 -> 处理 -> 销毁所有内存。
FrankenPHP Worker: 进程启动 -> 加载文件 -> 初始化应用 -> 进入循环处理请求 -> 请求结束 -> 内存保留 -> 等待下一个请求。
第一阶段:基础设施 (Docker & Caddyfile)
FrankenPHP 基于 Caddy Server。最简单的运行方式是使用 Docker。
1. 修改 Dockerfile
你需要基于 FrankenPHP 的官方镜像构建。
FROM dunglas/frankenphp
# 安装必要的 PHP 扩展 (根据你的项目需求添加)
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
# 复制你的项目代码
COPY . /app
WORKDIR /app
# 安装 Composer 依赖 (假设你有 composer.json)
ENV COMPOSER_ALLOW_SUPERUSER=1
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN composer install --no-dev --optimize-autoloader
# 设置环境变量,启用 Worker 模式
# FRANKENPHP_CONFIG 可以直接写 Caddy 配置,或者引用 Caddyfile
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
# 如果是生产环境,建议调整 worker 数量
# 默认是 CPU 核心数 * 2
# ENV SERVER_WORKER_COUNT=16
2. Caddyfile 配置 (可选)
如果你需要更复杂的路由配置,可以在项目根目录创建一个 Caddyfile:
{
# 全局配置
frankenphp {
# 定义 worker 脚本路径和并发数
worker ./public/index.php {
# 这是一个重要的设置:处理多少个请求后重启 Worker
# 这可以防止内存泄漏导致服务崩溃
max_requests 500
}
}
order php_server before file_server
}
:80 {
# 开启压缩
encode zstd gzip
# 静态文件处理
root * /app/public
# 执行 PHP
php_server
}
第二阶段:入口文件改造 (The Worker Script)
这是最关键的一步。你需要修改你的入口文件(通常是 public/index.php),将其包裹在 FrankenPHP 的处理循环中。
假设这是你原本的 index.php (伪代码):
<?php
require __DIR__ . '/../vendor/autoload.php';
$app = new MyApp();
$response = $app->handleRequest();
$response->send();
改造后的 index.php (Worker 模式):
<?php
require __DIR__ . '/../vendor/autoload.php';
// 1. 引导阶段 (Bootstrap)
// 这里的代码在 Worker 启动时只运行一次!
// 适合:加载配置、建立长连接(如Redis)、初始化容器
$app = new MyApp();
echo "Worker 启动成功\n";
// 2. 阻止脚本退出,进入 Worker 循环
// frankenphp_handle_request 是核心函数
$nbRequests = 0;
do {
$running = \frankenphp_handle_request(function () use ($app) {
// 3. 请求处理阶段 (Request Handling)
// 这里的代码每个请求都会运行
// 处理请求并输出内容
$app->handleRequest();
// 注意:不要在这里调用 exit() 或 die(),否则会杀掉整个 Worker 进程!
});
// 4. 垃圾回收 (非常重要)
// 在请求结束后手动触发 GC,防止内存缓慢增长
gc_collect_cycles();
$nbRequests++;
// 可选:如果不想用 Caddyfile 的 max_requests,也可以在这里手动控制重启
// if ($nbRequests > 500) break;
} while ($running);
第三阶段:代码逻辑重构 (排雷指南)
这是最耗时且最容易出 Bug 的部分。因为内存不再重置,你需要手动清理“状态”。
1. 静态变量 (Static Variables)
在 PHP-FPM 中,静态变量常用于缓存当前请求的数据。在 Worker 模式下,静态变量会跨请求持久化,导致数据污染。
function getCurrentUser() {
static $user; // 危险!第一个请求的用户会一直保留在这里
if (!$user) {
$user = fetchUserFromDb();
}
return $user;
}
```
* **修正方案:**
* **方案 A**: 避免使用 `static` 做请求级缓存。
* **方案 B**: 在请求结束时,显式重置静态变量(如果你的框架支持容器复位,这通常由框架处理)。
#### 2. 全局变量 (Global Variables)
虽然 FrankenPHP 会在每个请求开始时自动重置 `$_GET`, `$_POST`, `$_COOKIE` 等超全局变量,但如果你使用了自定义的 `global $myVar`,它可能会残留。
* **建议**:尽量避免使用 `global` 关键字。如果必须使用,确保在请求开始时对其进行初始化赋值。
#### 3. 数据库连接 (Database Connections)
* **长连接问题**:Worker 启动时建立的 DB 连接可能会在闲置一段时间后被数据库服务器断开(MySQL `wait_timeout`)。
* **解决方案**:
* 确保你的 DB 库有“断线重连”机制(Laravel/Doctrine 等现代框架通常都有)。
* 或者,不要在 Bootstrap 阶段连接 DB,而是在 Loop 内部(请求开始时)连接,请求结束时关闭(但这损失了部分性能优势)。
* **最佳实践**:使用连接池或保持长连接,但在执行 SQL 前检查连接活性 (`ping`)。
#### 4. 内存泄漏 (Memory Leaks)
这是 Worker 模式最大的敌人。
* **循环引用**:PHP 的 GC 可以处理大部分循环引用,但复杂的闭包或对象引用可能导致内存无法释放。
* **无限增长的数组**:检查是否有类似 `Logger::$logs[] = $msg` 这样的代码,如果不清理,这个数组会无限变大直到 OOM (Out of Memory)。
#### 5. `header()` 和 `setcookie()`
在标准 PHP 中,如果输出了内容再调用 `header()` 会报错。在 FrankenPHP 中,虽然它试图兼容,但最佳实践依然是:**严格区分逻辑层和视图层**,确保所有 Header 在任何 `echo` 之前设置。
#### 6. 文件上传
FrankenPHP 处理文件上传的方式与标准 PHP 略有不同,但它极力模拟了标准行为。
* 检查 `$_FILES` 数组通常能正常工作。
* 注意:上传的临时文件在请求结束后会被删除。如果你在 Worker 循环中通过异步方式处理文件,文件可能已经不存在了。
#### 7. `exit` 和 `die`
* **绝对禁止**:在业务逻辑中使用 `die()` 或 `exit()` 会直接杀掉当前的 Worker 进程。Caddy 会重启它,但这会导致性能抖动。
* **替代**:使用 `return` 或抛出异常来结束当前请求的处理逻辑。
---
### 第四阶段:针对框架的建议
如果你的网站使用了主流框架,情况会简单很多,因为社区已经有了适配器。
#### 1. Laravel 用户
不要手动改代码,直接使用 **Laravel Octane**。
bash
composer require laravel/octane
php artisan octane:install --server=frankenphp
``
Octane 已经帮你处理了容器重置、静态变量清理等脏活累活。
#### 2. Symfony 用户
使用 **Symfony Runtime** 组件。
Symfony 的 Runtime 组件原生支持 FrankenPHP,通常只需要设置环境变量 APP
RUNTIME=Symfony\Component\Runtime\FrankenPhpSymfonyRuntime。
#### 3. ThinkPHP / 其他遗留框架
你需要按照**第二阶段**的方法,手动编写一个 worker.php。你需要深入研究框架的生命周期:
1. 分离 App::init() (加载配置,只做一次)。
2. 分离 App::run() (处理请求,循环执行)。
3. 分离 App::clear() (清理请求级数据,手动实现)。
---
### 总结清单
1. **Docker 化**:使用 dunglas/frankenphp 镜像。
2. **入口改造**:用 frankenphphandle
request 包裹请求逻辑。
3. **清理 Statics**:检查所有 static 关键字,确保它们不会跨请求污染数据。
4. **禁止 Die**:全局搜索 die / exit 并替换为 return。
5. **配置重启策略**:在 Caddyfile 中设置 maxrequests
(如 500-1000),作为内存泄漏的兜底方案。
6. **测试**:使用 ab
或 wrk
进行压力测试,并观察内存占用(docker stats`)。如果内存一直涨不掉,说明有泄漏。