FrankenPHP Worker 模式深度研究报告——常驻内存革新 PHP 性能
目录
1. 概述:FrankenPHP 与其 Worker 模式 2. 底层架构:Go + CGO + PHP ZTS 三位一体 3. Worker 模式工作原理:常驻内存之奥妙 4. 线程模型与状态机:精密之生命周期管理 5. 性能基准测试:数据说话 6. 与传统 PHP-FPM 之对比:优劣分明 7. 配置与部署:三种路径 8. 限制与坑点:知其所以然 9. 最佳实践:过来人之经验 10. 适用场景与结论
---
1. 概述
1.1 FrankenPHP 为何物?
FrankenPHP 者,现代 PHP 应用服务器也。其将 Caddy Web 服务器与 PHP 运行时深度融合,以 CGO 桥接技术将 PHP 解释器直接嵌入 Go 进程,一气呵成,无需外部 PHP-FPM 进程。
核心特性如下:
- 单二进制文件,静态链接,无依赖部署
- 原生支持自动 HTTPS、HTTP/2、HTTP/3
- 支持 Early Hints(103 状态码)
- 内嵌 PHP 8.2–8.5,含常用扩展
- 可作为 Go 库嵌入任意
net/http应用
1.2 Worker 模式者何?
传统 PHP 运行于 CGI/FastCGI 模式,每来一请求,便启动一进程,加载框架,初始化服务容器,执行脚本,然后销毁——周而复始,大量 CPU 周期耗于重复初始化。
Worker 模式之核心思想:应用只启动一次,常驻内存。此后所有请求,皆复用已初始化之状态,直接处理,无需再经框架引导之开销。此举可将请求延迟降至毫秒级,吞吐量大幅提升。
> 譬若:传统模式如每次待客皆须重建厨房、购置食材;Worker 模式则厨房常备,厨师在位,来客即烹,其速可知。
---
2. 底层架构:Go + CGO + PHP ZTS 三位一体
2.1 三层架构总览
FrankenPHP 之架构,可分为三层:
┌─────────────────────────────────────────┐
│ Caddy 层(Go) │ HTTP 请求接入、路由、TLS
├─────────────────────────────────────────┤
│ FrankenPHP Go 层(CGO) │ 线程池管理、状态机、请求分发
├─────────────────────────────────────────┤
│ C 层(php_thread / SAPI) │ PHP 解释器嵌入、脚本执行
└─────────────────────────────────────────┘
| 层级 | 对应文件 | 核心职责 |
|---|---|---|
| Go 层 | frankenphp.go、phpthread.go、scaling.go | 线程池管理、请求路由、自动扩缩容 |
| C 层 | frankenphp.c、frankenphp.h | PHP SAPI 实现、脚本执行循环、超全局变量管理 |
| 状态机层 | internal/state/ | 同步 Go 协程与 C 线程之生命周期 |
2.2 CGO 桥接:Go 与 PHP 之对话
FrankenPHP 通过 CGO 直接将 PHP 解释器嵌入 Go 运行时。CGO 允许 Go 代码调用 C 函数,亦允许 C 代码回调 Go 函数。
关键机制:每个 PHP 执行线程均有一个 threadIndex(类型 C.uintptr_t),作为全局 phpThreads 数组之索引。所有 CGO 回调皆以此索引为第一参数,Go 代码借此获取当前请求对应之 phpThread 与 frankenPHPContext。
数据流向举例:
- PHP → Go(输出):
echo "hello"→php_output_write()→ SAPI 路由至frankenphp_ub_write()→ 调用 Go 之go_ub_write()→ 写入http.ResponseWriter - Go → PHP(输入):PHP 读取 POST 数据 → 调用
go_read_post()→ Go 从request.Body读取 → 写入 C 层缓冲区 → 返回给 PHP
2.3 ZTS:为何必须线程安全?
ZTS(Zend Thread Safety) 者,PHP 之线程安全构建也。
FrankenPHP 之所以必须使用 ZTS 版 PHP,原因在于:Go 之并发模型基于协程(goroutine),而 Go 运行时可将多个 goroutine 调度于同一 OS 线程,亦可将其分布于多个 OS 线程。当 PHP 在 Go 进程中运行时,若多个 goroutine 并发调用 PHP,则 PHP 须运行于真实之 POSIX 线程上,且须保证线程安全。
> 核心约束:FrankenPHP 中所有 PHP 执行线程均为操作系统级线程,非 Go 协程。Go 层仅负责这些线程之调度与生命周期管理。
---
3. Worker 模式工作原理:常驻内存之奥妙
3.1 请求处理循环
Worker 模式之核心,在于 PHP 脚本中运行一个 持久之请求处理循环。以下为最简示例:
<?php
// public/worker.php
// ① 应用初始化——仅执行一次
require __DIR__.'/vendor/autoload.php';
$app = new \App\Kernel();
$app->boot();
// ② 定义请求处理器(置于循环外,避免重复创建)
$handler = static function () use ($app) {
try {
echo $app->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
} catch (\Throwable $e) {
(new \App\ExceptionHandler)->handle($e);
}
};
// ③ 获取最大请求数配置(用于规避内存泄漏)
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
// ④ 请求处理循环——常驻于此
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
// 请求处理完毕后,执行应用终止逻辑
$app->terminate();
// 主动触发垃圾回收,避免请求处理中被触发而影响性能
gc_collect_cycles();
if (!$keepRunning) break;
}
// ⑤ Worker 退出前清理
$app->shutdown();
3.2 frankenphp_handle_request() 内部流程
此函数为 Worker 模式之核心,运行于 PHP 侧:
1. PHP Worker 脚本循环调用 frankenphp_handle_request()
2. 调用触发 Go 侧之 waitForWorkerRequest() 阻塞,直至有 HTTP 请求到达
3. Go 侧收到请求后,为该请求设置上下文(请求信息、环境变量等)
4. 函数返回,PHP 回调开始处理请求
5. 请求处理完成后,调用 go_frankenphp_finish_worker_request() 清理请求上下文
6. 函数返回 true,PHP 脚本循环回到步骤 1,等待下一请求
7. 若返回 false,说明线程需重启,PHP 脚本退出,触发重启流程
3.3 超全局变量之行为
Worker 模式中,超全局变量之行为与传统 PHP 迥异,须格外留意:
| 变量 | 行为 |
|---|---|
$_GET、$_POST、$_COOKIE、$_FILES、$_SERVER、$_REQUEST | 每次调用 frankenphp_handle_request() 时 自动重置 为当前请求之值,请求间相互隔离 |
$_ENV | 不会自动重置!请求中对 $_ENV 之修改会持久化至后续请求,切勿于此存储请求相关或敏感数据 |
| Worker 初始化时之超全局变量 | 第一次调用 frankenphp_handle_request() 前之值会保留。若需在请求回调中使用,须提前复制至闭包变量中 |
4. 线程模型与状态机:精密之生命周期管理
4.1 三类线程
FrankenPHP 包含三类线程,各自生命周期独立:
主线程(phpmainthread.go):
- 初始化 PHP 运行时,生命周期与服务器一致
- 执行步骤:应用
php.ini配置 → 快照环境至main_thread_env→ 启动 PHP SAPI 模块 → 向 Go 侧发送就绪信号
threadregular.go):
- 处理经典模式之一次性请求:每请求对应一次 PHP 脚本执行
- 执行流程:接收请求 →
beforeScriptExecution()→ C 层执行脚本 →afterScriptExecution()回收
threadworker.go):
- 处理Worker 模式:PHP 脚本可跨多个请求保持存活
- 重启规则:只要脚本至少成功执行过一次
frankenphp_handle_request(),退出后(无论正常或致命错误)会立即重启;唯连续启动失败(脚本从未到达frankenphp_handle_request())时,才应用指数退避策略
4.2 线程状态机
每个线程之状态由 internal/state/state.go 中定义之 ThreadState 管理,所有状态转换通过 sync.RWMutex 保证原子性。
完整状态列表:
| 状态 | 描述 |
|---|---|
Reserved | 线程槽已分配但未启动 |
BootRequested | 启动已排队,POSIX 线程尚未运行 |
Booting | 底层 POSIX 线程正在启动 |
Inactive | 线程存活但未分配处理器,内存占用最小 |
Ready | 已分配处理器,可接收请求 |
ShuttingDown | 线程正在关闭 |
Done | 线程完全关闭,可转回 Reserved 复用 |
Restarting | 工作线程正在重启(通过管理 API 或文件监控触发) |
Rebooting | 工作线程退出 C 循环执行完整 ZTS 重启 |
Yielding | 工作线程已让出控制权,等待重新激活 |
4.3 自动扩缩容机制
FrankenPHP 支持线程池之自动扩缩容(scaling.go):
扩容条件(须同时满足):
- 请求已阻塞至少 5ms
- CPU 使用率低于 80%
- 未达到
max_threads限制
- 每 5 秒检查一次空闲之自动扩容线程
- 处于
Ready状态且空闲时间超过maxIdleTime(默认 5 秒)之线程,会被转换为Inactive状态(每次最多转换 10 个)
5. 性能基准测试:数据说话
5.1 Ivan Vulovic 之基准测试(2025年7月)
> 此测试聚焦 Worker 模式,对比 FrankenPHP Worker 与 Nginx + PHP-FPM。
测试环境:
- AWS EC2 专用实例,6 vCPU + 4GB RAM
- PHP 8.4,隔离 Docker 容器
- 测试工具:
wrk、wrk2、k6 - 测试代码:极简
index.php(仅echo,无框架)
| 测试工具 | 指标 | FrankenPHP Worker | PHP-FPM | 优势倍数 |
|---|---|---|---|---|
wrk(最大吞吐量) | RPS | ~15,000 | ~4,000 | 3.75x |
wrk(最大吞吐量) | 平均延迟 | ~15ms | ~30ms | 2x |
wrk2(固定 5000 RPS) | 实际处理 | ~5000 RPS | ~3790 RPS | — |
wrk2(固定 5000 RPS) | 平均延迟 | ~1.4ms | ~860ms | 614x |
k6(100 虚拟用户) | 平均延迟 | <2ms | 2.56ms | 1.28x |
k6(100 虚拟用户) | P95 延迟 | ~3.5ms | ~6ms | 1.71x |
5.2 Tideways 之基准测试(经典模式,2025年9月)
> Tideways(PHP 性能监控服务商)之测试聚焦经典模式(非 Worker 模式),结论更为保守。
测试环境:
- Hetzner CCX33 VPS,8 核 vCPU(AMD EPYC)
- Debian 13,PHP 8.4
- 压测工具:Vegeta
| 测试场景 | PHP-FPM RPS | FrankenPHP RPS | 差异 |
|---|---|---|---|
| Hello World(最小响应) | 18,479 | 18,404 | 仅 0.4% |
| HTML 响应(50KiB) | 7,023 | 6,934 | PHP-FPM 快 1.3% |
| PDF 响应(大文件) | 7,610 | 5,369 | PHP-FPM 快 29.4% |
| Hello World(100 并发) | 21,848 | 22,675 | FrankenPHP 快 3.7% |
5.3 两种测试结论之矛盾何在?
两篇基准测试之结论看似矛盾,实则不然:
| 维度 | Ivan Vulovic 测试 | Tideways 测试 |
|---|---|---|
| 测试模式 | Worker 模式 | 经典模式 |
| 对比对象 | FrankenPHP Worker vs PHP-FPM | FrankenPHP 经典模式 vs PHP-FPM |
| 结论 | Worker 模式性能大幅领先 | 经典模式性能与 FPM 相当 |
---
6. 与传统 PHP-FPM 之对比:优劣分明
6.1 全面对比表
| 对比维度 | 传统 Nginx + PHP-FPM | FrankenPHP 经典模式 | FrankenPHP Worker 模式 |
|---|---|---|---|
| 进程模型 | 每请求一进程(或进程池) | Go 线程池 | 长驻 PHP 进程 |
| 应用初始化 | 每请求执行一次 | 每请求执行一次 | 仅一次,常驻内存 |
| 请求延迟 | 高(受初始化开销影响) | 与 FPM 相当 | 毫秒级,极低 |
| 吞吐量(RPS) | 中等 | 与 FPM 相当 | 3–4 倍于 FPM |
| 内存占用 | 高(每进程独立加载应用) | 与 FPM 相当 | 更低(代码共享) |
| 部署复杂度 | 高(Nginx + FPM + 配置) | 低(单二进制) | 低(单二进制) |
| 代码兼容性 | 完全兼容 | 完全兼容 | 需适配长生命周期 |
| 内存泄漏风险 | 无(进程销毁即清理) | 无 | 有(须定期重启) |
| 热重载支持 | 需第三方工具 | 原生 --watch 支持 | 原生 --watch 支持 |
| 状态共享 | 需外部存储 | 需外部存储 | 需外部存储 |
| 调试复杂度 | 低 | 低 | 高(长生命周期) |
6.2 架构复杂度对比
Nginx + PHP-FPM:
HTTP 请求 → Nginx → Unix Socket/TCP → PHP-FPM → PHP 解释器 → 响应
(共 4 次上下文切换)
FrankenPHP Worker 模式:
HTTP 请求 → Caddy(Go)→ CGO 调用 → PHP 线程(常驻)→ 响应
(无进程间通信,一次 CGO 调用)
---
7. 配置与部署:三种路径
7.1 Docker 部署(推荐)
# 基础配置:使用默认 Worker 数量(每 CPU 2 个)
docker run \
-e FRANKENPHP_CONFIG="worker /app/public/worker.php" \
-v $PWD:/app \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
# 自定义 Worker 数量:启动 42 个 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
7.2 独立二进制部署
# 基础启动
frankenphp php-server --worker /path/to/worker.php
# 开启文件变更监听(开发环境)
frankenphp php-server --worker /path/to/worker.php --watch="/path/to/app/**/*.php"
7.3 Caddyfile 高级配置
frankenphp {
worker {
# 最大连续失败次数,超过后 FrankenPHP 会崩溃报错
max_consecutive_failures 10
}
}
7.4 框架集成
Symfony 7.4+:原生支持 Worker 模式,无需额外配置。
Laravel:通过 Laravel Octane 集成,参考官方文档。
通用自定义 Worker:不依赖第三方库,适配所有 PHP 框架(示例见第三节)。
---
8. 限制与坑点:知其所以然
8.1 内存泄漏之隐患
PHP 最初设计用于短生命周期之 CGI 模式,部分传统代码/库可能存在内存泄漏问题。Worker 进程常驻内存,泄漏会随时间累积,最终致 OOM 崩溃。
解决方案:
1. 设置 MAX_REQUESTS 环境变量,处理一定数量请求后自动重启 Worker
2. 使用 --watch 参数,开发环境文件变更时自动重启
3. 手动通过 Admin API 重启:curl -X POST http://localhost:2019/frankenphp/workers/restart
8.2 状态污染之陷阱
由于 PHP 进程常驻,以下状态会在请求间保留:
- 函数/方法内之
static静态变量 - 类之静态属性
- Worker 脚本全局作用域之变量
- 内存中的缓存数据
8.3 $_ENV 不会自动重置
$_ENV 在 Worker 模式中 不会 在每个请求后重置,请求中对 $_ENV 的修改会持久化至后续请求。切勿于此存储请求相关或敏感数据。
8.4 异常捕获之特殊要求
请求处理回调中 必须手动捕获所有异常,不可依赖 set_exception_handler——因为该函数在 Worker 脚本结束时才会触发,而非每个请求后触发。
8.5 进程间状态不共享
Worker 进程之间相互独立,无法共享内存中之状态。须使用外部存储(如 Redis、Memcached)实现数据共享。
---
9. 最佳实践:过来人之经验
9.1 代码层面
1. 请求处理器外置:将请求处理逻辑定义在循环外部,避免每次循环都重复创建闭包/对象
2. 主动垃圾回收:每次请求处理完成后主动调用 gc_collect_cycles(),降低请求处理过程中触发 GC 之概率
3. 合理设置最大请求数:对于存在内存泄漏风险之代码,设置合理之 MAX_REQUESTS 阈值
4. 避免全局状态污染:不要在 Worker 初始化阶段修改全局状态
5. 请求回调中捕获所有异常:避免异常导致 Worker 进程退出
9.2 配置层面
1. 开发环境开启文件监听:使用 --watch 参数,文件修改后自动重启 Worker
2. 生产环境配置健康检查:结合 Caddy 之健康检查功能,确保 Worker 进程异常时能自动恢复
3. 合理设置 Worker 数量:根据 CPU 核心数与应用负载调整,一般建议设置为 CPU 核心数之 1–4 倍
4. 配置 max_consecutive_failures:避免异常脚本导致服务整体崩溃
9.3 监控层面
1. 监控 Worker 进程内存使用:设置告警,内存持续增长时及时排查 2. 监控请求延迟分布:关注 P99 延迟,及时发现性能退化 3. 日志收集:Worker 进程之错误日志须集中收集,便于排查问题
---
10. 适用场景与结论
10.1 适合使用 FrankenPHP Worker 模式的场景
| 场景 | 建议 |
|---|---|
| 新项目 | ✅ 建议直接采用,可降低架构复杂度 |
| API 服务 / 微服务 | ✅ 性能优势明显,部署简洁 |
| 使用 Symfony / Laravel 之项目 | ✅ 官方已提供集成,适配成本低 |
| 高并发、低延迟要求之服务 | ✅ Worker 模式之性能优势明显 |
| 需要与 Go 生态集成 | ✅ 可作为 Go 库嵌入 |
10.2 暂不适合之场景
| 场景 | 建议 |
|---|---|
| 大型遗留项目(大量全局状态) | ❌ 适配成本高,须充分评估 |
| 代码质量差、内存泄漏严重 | ❌ 须先重构代码,再考虑迁移 |
| 团队对 PHP 长生命周期运行无经验 | 🟡 须先培训,或暂缓迁移 |
| 依赖特定 Nginx 模块之功能 | 🟡 须确认 Caddy 是否有对应功能 |
10.3 总结
FrankenPHP 之 Worker 模式,实为 PHP 生态之一大革新。其通过常驻内存之方式,令 PHP 应用之性能表现可媲美 Go、Node.js 等现代语言之应用。
然须明了:Worker 模式之性能优势,来源于应用初始化之省略,非 PHP 语言本身之加速。若应用无法适配长生命周期(如大量使用全局状态、存在内存泄漏),则须先重构代码,再考虑迁移。
最终建议: 1. 新项目可直接采用 FrankenPHP + Worker 模式 2. 已有项目须先做适配性评估,再决定是否需要迁移 3. 若暂无法适配 Worker 模式,FrankenPHP 经典模式仍可作为 Nginx + PHP-FPM 之替代品,简化部署流程
---
参考资源
- 官方文档:https://frankenphp.dev/zh/docs/worker/
- 内部架构文档:https://frankenphp.dev/docs/internals/
- Ivan Vulovic 基准测试:https://vulovic.me/frankenphp-vs-php-fpm-benchmarks-surprises-and-one-clear-winner
- Tideways 基准测试:https://tideways.com/profiler/blog/testing-if-franken-php-classic-mode-is-faster-and-more-scalable-than-php-fpm
- GitHub 仓库:https://github.com/php/frankenphp
🌟 智谱 GLM-5 已上线
我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。
🎁 领取 2000万 Tokens