← 返回主题列表
Q
QianXun
@QianXun · 2026年06月14日 17:33 · 2浏览

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.gophpthread.goscaling.go线程池管理、请求路由、自动扩缩容
C 层frankenphp.cfrankenphp.hPHP 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 代码借此获取当前请求对应之 phpThreadfrankenPHPContext

数据流向举例

  • 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 容器
  • 测试工具:wrkwrk2k6
  • 测试代码:极简 index.php(仅 echo,无框架)
测试结果

测试工具指标FrankenPHP WorkerPHP-FPM优势倍数
wrk(最大吞吐量)RPS~15,000~4,0003.75x
wrk(最大吞吐量)平均延迟~15ms~30ms2x
wrk2(固定 5000 RPS)实际处理~5000 RPS~3790 RPS
wrk2(固定 5000 RPS)平均延迟~1.4ms~860ms614x
k6(100 虚拟用户)平均延迟<2ms2.56ms1.28x
k6(100 虚拟用户)P95 延迟~3.5ms~6ms1.71x
关键发现:PHP-FPM 在固定 5000 RPS 之高压力下完全无法支撑,延迟飙升至近 1 秒,而 FrankenPHP 仍能保持毫秒级响应。

5.2 Tideways 之基准测试(经典模式,2025年9月)

> Tideways(PHP 性能监控服务商)之测试聚焦经典模式(非 Worker 模式),结论更为保守。

测试环境

  • Hetzner CCX33 VPS,8 核 vCPU(AMD EPYC)
  • Debian 13,PHP 8.4
  • 压测工具:Vegeta
测试结果(60 秒压测)

测试场景PHP-FPM RPSFrankenPHP RPS差异
Hello World(最小响应)18,47918,404仅 0.4%
HTML 响应(50KiB)7,0236,934PHP-FPM 快 1.3%
PDF 响应(大文件)7,6105,369PHP-FPM 快 29.4%
Hello World(100 并发)21,84822,675FrankenPHP 快 3.7%
Tideways 结论:FrankenPHP 经典模式与 PHP-FPM 之运行开销差异极小,不足以成为迁移之核心理由。应用架构对性能之影响,远大于运行时之选择。

5.3 两种测试结论之矛盾何在?

两篇基准测试之结论看似矛盾,实则不然:

维度Ivan Vulovic 测试Tideways 测试
测试模式Worker 模式经典模式
对比对象FrankenPHP Worker vs PHP-FPMFrankenPHP 经典模式 vs PHP-FPM
结论Worker 模式性能大幅领先经典模式性能与 FPM 相当
核心要点:FrankenPHP 之性能优势,主要来源于 Worker 模式之常驻内存机制,非经典模式。若应用无法适配 Worker 模式,则性能提升有限。

---

6. 与传统 PHP-FPM 之对比:优劣分明

6.1 全面对比表

对比维度传统 Nginx + PHP-FPMFrankenPHP 经典模式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 脚本全局作用域之变量
  • 内存中的缓存数据
须主动重置请求相关之临时状态,避免跨请求污染。Symfony/Laravel 等框架已内置大部分状态重置逻辑。

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
---

👍 1
💬 讨论回复 (0)
推荐

🌟 智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

🎁 领取 2000万 Tokens