news 2026/9/15 17:41:47

FrankenPHP Worker 模式完全指南:一次启动、常驻内存的 PHP 应用服务器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FrankenPHP Worker 模式完全指南:一次启动、常驻内存的 PHP 应用服务器

FrankenPHP Worker 模式完全指南:一次启动、常驻内存的 PHP 应用服务器

【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp

导读

本文以 FrankenPHP 官方文档(docs/it/worker.md)为主体,系统讲解 Worker 模式的启动方式、自定义 Worker 编写、崩溃恢复、超全局变量行为与状态持久化等核心机制,并辅以仓库源码(worker.gothreadworker.gocaddy/workerconfig.gocaddy/admin.go)作为实现依据。读完本文,你将掌握如何把 PHP 应用一次性加载进内存、在毫秒级响应请求,并能在生产环境中正确配置、监控与重启 Worker。


一、Worker 模式是什么

传统 PHP 的每个请求都会重新加载应用、重新解析代码。Worker 模式则相反:应用只启动一次,随后常驻内存,由 FrankenPHP 负责把到达的 HTTP 请求快速分发到已就绪的 Worker 进程/线程上处理,从而把每请求的开销降到最低。

从源码看,worker.go中每个worker结构体代表"一个 Worker 脚本",可以挂载多个线程:

// worker.go type worker struct { mercureContext name string fileName string num int maxThreads int ... maxConsecutiveFailures int ... }

Worker 脚本通过 C 扩展暴露的frankenphp_handle_request()函数接收请求并返回响应,每次调用会阻塞直到下一个请求到来。


二、启动 Worker

2.1 使用 Docker 启动

设置环境变量FRANKENPHP_CONFIGworker /path/to/your/worker/script.php

docker run \ -e FRANKENPHP_CONFIG="worker /app/path/to/your/worker/script.php" \ -v $PWD:/app \ -p 80:80 -p 443:443 -p 443:443/udp \ dunglas/frankenphp

FRANKENPHP_CONFIG中的路径是容器内路径,因此示例中与-v $PWD:/app的挂载点保持一致。

2.2 使用独立二进制启动

使用php-server命令的--worker选项,即可用 Worker 模式伺服当前目录内容:

frankenphp php-server --worker /path/to/your/worker/script.php

如果 PHP 应用已嵌入到二进制文件中,可以在应用根目录添加自定义Caddyfile,它会被自动采用,无需额外传参。

2.3 监听文件变化自动重启(--watch)

Worker 常驻内存后,代码修改不会自动生效。可以用--watch选项在文件变化时自动重启 Worker,详见 config.md 的文件变化控制。

下面的命令会在/path/to/your/app/及其子目录中任何.php文件被修改时触发重启:

frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to/your/app/**/*.php"

--watch经常与热重载(Hot Reload)搭配使用:前者负责重启 Worker,后者负责刷新浏览器页面,实现接近"保存即生效"的开发体验。


三、框架集成

3.1 Symfony

Symfony 用户请直接参考 Symfony Worker 模式文档,其中包含与 FrankenPHP 结合的具体配置与命令。

3.2 Laravel Octane

Laravel 用户可参考 Laravel Octane 文档,FrankenPHP 是 Octane 官方支持的服务器之一。


四、编写自定义 Worker

不依赖任何第三方库,用纯 PHP 就能写出自己的 Worker。下面是官方文档提供的完整示例:

<?php // public/index.php // Avvio dell'applicazione require __DIR__.'/vendor/autoload.php'; $myApp = new \App\Kernel(); $myApp->boot(); // Gestore fuori dal ciclo per migliori prestazioni (meno lavoro per richiesta) $handler = static function () use ($myApp) { try { // Chiamato quando arriva una richiesta, // i superglobali, php://input e simili vengono reimpostati echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER); } catch (\Throwable $exception) { // `set_exception_handler` viene chiamato solo quando termina lo script worker, // il che potrebbe non essere il comportamento atteso, quindi intercettare e gestire qui le eccezioni (new \MyCustomExceptionHandler)->handleException($exception); } }; $maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { $keepRunning = \frankenphp_handle_request($handler); // Esegui qualcosa dopo aver inviato la risposta HTTP $myApp->terminate(); // Richiama il garbage collector per ridurre la probabilità che parta durante la generazione di una pagina gc_collect_cycles(); if (!$keepRunning) break; } // Pulizia $myApp->shutdown();

代码要点:

  • 启动逻辑放循环外require autoloadboot()等昂贵操作只执行一次,这是性能提升的关键;
  • $handler是静态闭包:每个请求只调用闭包本身,避免每次重建;
  • 异常必须自行捕获:文档特别指出,set_exception_handler只有在 Worker 脚本退出时才会触发,与直觉不符,所以要在循环内拦截并处理;
  • gc_collect_cycles()手动回收:在响应发送后、等待下一个请求前主动触发,降低页面生成过程中 GC 介入的概率;
  • $keepRunning返回值frankenphp_handle_request()返回 false 时退出循环,进行优雅清理(shutdown())。

随后用FRANKENPHP_CONFIG环境变量指向该脚本启动:

docker run \ -e FRANKENPHP_CONFIG="worker ./public/index.php" \ -v $PWD:/app \ -p 80:80 -p 443:443 -p 443:443/udp \ dunglas/frankenphp

4.1 Worker 数量:默认 2 个/CPU,可显式指定

默认情况下,每个 CPU 核心启动 2 个 Worker。也可以在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

对应到源码,caddy/workerconfig.go中的worker指令还支持更细粒度的 Caddyfile 配置项:

子指令说明
nameWorker 名称,默认取文件名
fileWorker 脚本路径(必填)
num启动的 Worker 数量
max_threads该 Worker 允许的最大线程数,超过后不再扩容(见 scaling.go 的扩容逻辑)
env为 Worker 注入额外环境变量,可重复指定
watch监听的文件/目录模式,留空使用默认模式
match按请求路径匹配该 Worker
max_consecutive_failures最大连续失败次数(详见下文)

4.2 每处理 N 个请求后自动重启(MAX_REQUESTS)

PHP 语言本身并非为常驻进程设计,大量旧库和遗留代码存在内存泄漏。在 Worker 模式下应对这类代码的通用做法是:处理完一定数量的请求后主动重启 Worker

上面的示例代码已经内置了该能力——通过环境变量MAX_REQUESTS设置上限:

$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {

MAX_REQUESTS为 0(默认)时不限制;设置为 N 时,Worker 处理完 N 个请求后正常退出,由 FrankenPHP 拉起新的 Worker 进程,从而周期性回收内存。


五、手动重启 Worker(Caddy Admin API)

除了按文件变化自动重启之外,还可以通过 Caddy 的管理 API 中启用了 Admin 端点。

发送一条简单的 POST 请求即可:

curl -X POST http://localhost:2019/frankenphp/workers/restart

该端点定义在 caddy/admin.go 中:

// EXPERIMENTAL: These routes are not yet stable and may change in the future. func (admin FrankenPHPAdmin) Routes() []caddy.AdminRoute { return []caddy.AdminRoute{ { Pattern: "/frankenphp/workers/restart", Handler: caddy.AdminHandlerFunc(admin.restartWorkers), }, { Pattern: "/frankenphp/threads", Handler: caddy.AdminHandlerFunc(admin.threads), }, } }

注意三点:

  • 该路由仅接受 POST,其他方法返回 405;成功时响应"workers restarted successfully\n",对应测试见 caddy/admin_test.go;
  • 源码注释明确标注EXPERIMENTAL(实验性),路由在未来版本可能变化;
  • 同模块还提供/frankenphp/threads端点,可查看各线程的运行状态快照。

底层实现调用frankenphp.RestartWorkers()(见 worker.go),它会等待所有 Worker 线程让出控制权后统一重启——源码注释强调"所有 Worker 必须同时重启,以避免 opcache 重置带来的不一致问题"。


六、Worker 崩溃处理与指数退避

如果 Worker 以非零退出码崩溃,FrankenPHP 会按**指数退避(exponential backoff)**策略重启它。判断逻辑如下:

  • 若 Worker 存活时间超过"上一次退避时长 × 2",则不被惩罚,直接再次重启;
  • 若 Worker 在短时间内持续以非零退出码失败(例如脚本存在拼写错误),FrankenPHP 最终会以错误too many consecutive failures停止。

源码实现位于 threadworker.go:

// wait a bit and try again (exponential backoff) backoffDuration := time.Duration(handler.failureCount*handler.failureCount*100) * time.Millisecond if backoffDuration > time.Second { backoffDuration = time.Second } handler.failureCount++ time.Sleep(backoffDuration)

即退避时长按失败次数的平方增长:1 次失败约 100ms、2 次约 400ms、3 次约 900ms……封顶 1 秒。触发终止的判定:

if worker.maxConsecutiveFailures >= 0 && startupFailChan != nil && !watcherIsEnabled && handler.failureCount >= worker.maxConsecutiveFailures { startupFailChan <- fmt.Errorf("too many consecutive failures: worker %s has not reached frankenphp_handle_request()", worker.fileName) handler.thread.state.Set(state.ShuttingDown) return }

6.1 配置 max_consecutive_failures

最大连续失败次数可在 Caddyfile 的frankenphp块中配置:

frankenphp { worker { # ... max_consecutive_failures 10 } }

依据 caddy/workerconfig.go:

  • 默认值为 6
  • 设为-1 表示永不因连续失败而终止set to -1 to never panick);
  • 取值必须>= -1,否则 Caddyfile 解析直接报错(max_consecutive_failures must be >= -1);
  • --watch监听模式开启时,该上限判定会被跳过(!watcherIsEnabled),因为脚本失败更可能是文件改动所致。

七、超全局变量(Superglobals)的行为

PHP 超全局变量($_SERVER$_ENV$_GET等)在 Worker 模式下遵循以下规则:

  • 在第一次调用frankenphp_handle_request()之前,超全局变量保存的是Worker 脚本自身的值;
  • frankenphp_handle_request()调用期间及之后,超全局变量保存的是当前 HTTP 请求的值;每次调用都会用新请求的值覆盖它们。

如果要在回调内访问 Worker 脚本自己的超全局变量,必须在第一次调用前复制,并导入闭包作用域:

<?php // Copia il superglobale $_SERVER del worker prima della prima chiamata a frankenphp_handle_request() $workerServer = $_SERVER; $handler = static function () use ($workerServer) { var_dump($_SERVER); // $_SERVER legato alla richiesta var_dump($workerServer); // $_SERVER dello script worker }; // ...

7.1 重要差异:$_ENV 不会被重置

大部分超全局变量($_GET$_POST$_COOKIE$_FILES$_SERVER$_REQUEST)会在请求之间自动重置

$_ENV目前不会在请求之间重置。这意味着:

  • 某次请求中对$_ENV的任何修改,都会持续存在,并对同一 Worker 线程后续处理的请求可见;
  • 因此不要$_ENV中存放敏感数据或请求专属数据。

这是当前实现的行为约束(原文档明确标注"attualmente"即"目前"),未来版本可能改变,写 Worker 时请务必留意。


八、状态持久化:Worker 模式的性能之源与副作用之源

由于 Worker 模式让 PHP 进程在请求之间保持存活,以下状态会跨请求持续存在

  • 静态变量:函数/方法内以static声明的变量,其值跨请求保留;
  • 类静态属性:类上的static属性跨请求保留;
  • 全局变量:Worker 全局作用域中的变量跨请求保留;
  • 内存缓存:请求处理器外部存储于内存中的任何数据(数组、对象)都会保留。

这是设计使然,也正是 Worker 模式快的原因——但要警惕副作用。官方文档给出了经典反例:

<?php function getCounter(): int { static $count = 0; return ++$count; // Incrementa tra le richieste! } $handler = static function () { echo getCounter(); // 1, 2, 3, ... per ogni richiesta su questo thread }; while (\frankenphp_handle_request($handler)) { // ... }

这段代码会按请求依次输出 1、2、3……因为$count在请求间持续递增。

编写 Worker 时,务必在每个请求之间重置一切请求专属状态。Symfony 与 Laravel Octane 等框架会自动恢复大部分状态,但某些服务仍可能需要手动重置:

  • Symfony 中,持有请求专属状态的服务应实现Symfony\Contracts\Service\ResetInterface,以便内核在请求之间调用其reset()方法。

九、进一步阅读

  • 官方 Worker 文档原文:docs/it/worker.md(另有 docs/worker.md 等语言版本)
  • 文件监听与 Caddyfile 配置:docs/it/config.md
  • 热重载:docs/it/hot-reload.md
  • Symfony 集成:docs/it/symfony.md
  • Laravel Octane 集成:docs/it/laravel.md
  • Worker 核心实现:worker.go、threadworker.go
  • Caddyfileworker指令解析:caddy/workerconfig.go
  • 管理 API 重启端点:caddy/admin.go 及其测试 caddy/admin_test.go
  • 指标(frankenphp_ready_workers等)可辅助观测 Worker 就绪状态:metrics.go

【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 17:38:45

kubeasz 混合架构集群部署实战:在 amd64 集群中平滑加入 arm64 节点

kubeasz 混合架构集群部署实战&#xff1a;在 amd64 集群中平滑加入 arm64 节点 【免费下载链接】kubeasz 使用Ansible脚本安装K8S集群&#xff0c;介绍组件交互原理&#xff0c;方便直接&#xff0c;不受国内网络环境影响 项目地址: https://gitcode.com/GitHub_Trending/ku…

作者头像 李华
网站建设 2026/9/15 17:37:49

dotnet/skills一键升级MSTest:v1/v2到v3迁移完全指南

dotnet/skills一键升级MSTest&#xff1a;v1/v2到v3迁移完全指南 【免费下载链接】skills Repository for skills to assist AI coding agents with .NET and C# 项目地址: https://gitcode.com/GitHub_Trending/skills17/skills 还在手动排查 MSTest 升级后的编译错误吗…

作者头像 李华
网站建设 2026/9/15 17:37:42

Qt散点图实现:基于QGraphicsView的高性能交互可视化方案

简介&#xff1a;这是一份面向Qt开发者的散点图实现源码示例。它聚焦于QGraphicsView图形视图框架&#xff0c;适合需要掌握自定义二维数据可视化、或想用C在Qt中绘制动态散点图的中初级开发者。压缩包体积仅6KB&#xff0c;共含5个文件&#xff0c;其中包含2个cpp源文件、1个头…

作者头像 李华
网站建设 2026/9/15 17:37:25

Unity卡通渲染利器UTS:从光照原理到参数实战全解析

做二次元角色渲染的人&#xff0c;多少都会绕不开一个名字&#xff1a;Unity-Chan Toon Shader&#xff0c;简称UTS。我最早接触它&#xff0c;是因为想复刻Unity酱那套经典的日式卡通脸部光照&#xff0c;结果发现网上教程要么只讲怎么拖参数、要么直接甩一份Shader源码让人自…

作者头像 李华
网站建设 2026/9/15 17:37:06

Pocket TTS接入Home Assistant语音助手:Wyoming协议实战指南

Pocket TTS接入Home Assistant语音助手&#xff1a;Wyoming协议实战指南 【免费下载链接】pocket-tts A TTS that fits in your CPU (and pocket) 项目地址: https://gitcode.com/GitHub_Trending/po/pocket-tts 想给 Home Assistant 配一个完全本地、低延迟的语音合成引…

作者头像 李华