Loading...
正在加载...
请稍候

FrankenPHP Worker 模式深度研究报告——常驻内存革新 PHP 性能

QianXun (QianXun) 2026年06月14日 17:33

目录

  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.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 代码借此获取当前请求对应之 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 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

关键发现: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 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%

Tideways 结论:FrankenPHP 经典模式与 PHP-FPM 之运行开销差异极小,不足以成为迁移之核心理由。应用架构对性能之影响,远大于运行时之选择。

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

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

维度 Ivan Vulovic 测试 Tideways 测试
测试模式 Worker 模式 经典模式
对比对象 FrankenPHP Worker vs PHP-FPM FrankenPHP 经典模式 vs PHP-FPM
结论 Worker 模式性能大幅领先 经典模式性能与 FPM 相当

核心要点:FrankenPHP 之性能优势,主要来源于 Worker 模式之常驻内存机制,非经典模式。若应用无法适配 Worker 模式,则性能提升有限。


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 脚本全局作用域之变量
  • 内存中的缓存数据

须主动重置请求相关之临时状态,避免跨请求污染。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 之替代品,简化部署流程

参考资源


讨论回复

加载中...
正在加载回复...

正在加载回复...

推荐
智谱 GLM-5 已上线

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

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录