FrankenPHP 上生产:worker 模式、嵌入二进制,以及它省掉的那几层
二十年来的“部署 PHP”基本是同一个三角:前面是 Apache 或 Nginx,后面是 PHP-FPM,中间粘着一份团队里没人真正看懂的配置文件。FrankenPHP 是这套栈很久以来第一个真正不同的答案,经过两年稳定发版,已经值得在生产环境认真考虑。如果 1.0 之后没再关注,项目已经走了很远:worker 模式够稳,嵌入二进制确实用于真实应用,Laravel 和 Symfony 的集成平稳得没什么可说的。
项目信息:
- 官网:https://frankenphp.dev
- GitHub:https://github.com/dunglas/frankenphp
- Laravel Octane 文档:https://laravel.com/docs/octane
- Symfony Runtime 组件:https://symfony.com/doc/current/components/runtime.html
- Mercure 协议:https://mercure.rocks
下面是把它放到真实应用前面时,真正要紧的部分。
它为什么存在
FrankenPHP 是构建在 Caddy Web 服务器之上的 PHP 应用服务器,用 Go 编写,PHP 作为库被嵌入,而不是作为独立进程执行。架构选择就是全部故事。PHP 跑在 Caddy 的进程里,所以没有 FastCGI 跳转,没有 socket 协商,没有另一套生命周期需要监控。你得到一个二进制:它以自动 HTTPS 服务 HTTP/1、HTTP/2、HTTP/3,顺便把你的 PHP 应用跑了。
招牌收益是性能,但真正的收益是运维简单:一个进程、一份配置、一个健康检查。在团队花几周为 Nginx、PHP-FPM 和 sidecar 配 Kubernetes 探针的世界里,单二进制模型是实打实的减负。
Worker 模式才是重点
传统 PHP 每个请求都要启动一次框架。对 Laravel 或 Symfony 来说,意味着加载上百个类、构建容器、启动 service provider——只为了处理一个业务逻辑可能两毫秒就结束的请求。worker 模式把框架留在内存里跨请求复用。
在 Laravel 项目里启用 worker 模式,改动小得离谱:
FROM dunglas/frankenphp
ENV SERVER_NAME=":80"
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
COPY . /app
WORKDIR /app
RUN composer install --no-dev --optimize-autoloader
worker 指令告诉 FrankenPHP 让 public/index.php 保持常驻。Laravel 通过 laravel/octane 提供一等支持,配置成 FrankenPHP 即可:
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:start --server=frankenphp --workers=4
Symfony 有对等的能力,走 runtime 组件和 php-runtime/frankenphp-symfony 包。两个框架上,CPU 密集型端点大约有 3 到 10 倍吞吐提升,启动开销大的应用数字还要更高。
代价很重要:worker 模式会把你之前蒙混过去的所有全局状态暴露出来。静态属性跨请求保留,容器绑定保留。你改了一个单例,下一个请求就看得见。多数 Laravel 和 Symfony 代码没问题——框架处理了请求/响应隔离——但你自己的代码需要审一遍。Octane 文档里有一份简短清单,上线前读掉。
嵌入二进制用于真实分发
embed 特性把整个 PHP 应用连同运行时打包成一个静态二进制。最后你手上是一个文件,丢到服务器、笔记本或容器里,它就跑你的应用。
# 在 FrankenPHP 仓库中操作,应用放在 app/ 目录下
docker buildx bake --load static-builder-gnu
./dist/frankenphp-linux-x86_64 php-server --root app/public
内部工具、自托管 SaaS,以及任何“给我一个安装包”就算卖点的场景,这条路都成立。Symfony CLI 已经用基于 FrankenPHP 的二进制发布了一段时间,这套工作流也不只适用于小应用。
注意:不小心的话,嵌入二进制里会带上你的密钥。运行时用环境变量,别把配置烤进去;即便私下分发,也把这个产物当作公开代码对待。
HTTP/3、Early Hints 和 Mercure
因为 Caddy 本来就讲现代 Web,FrankenPHP 直接继承了那些在 PHP-FPM 上要费大力气才能加的特性。有有效 TLS 证书时 HTTP/3 默认开启。Early Hints(HTTP 103)让你在 PHP 还在构建响应时,就把 CSS 和关键资源推给浏览器——Laravel 的 helper 在 11.x 落地,Symfony 的 WebLink 组件支持已久。
Mercure 是服务端事件与实时更新的协议,内建在 FrankenPHP 里。仪表盘、通知、实时更新的 UI,不想背 WebSocket 复杂度时,这是目前 PHP 生态里最轻的选项。发布一次更新,就是 worker 里一个 HTTP POST:
use Symfony\Component\Mercure\Update;
$update = new Update(
'https://example.com/orders/42',
json_encode(['status' => 'shipped'])
);
$hub->publish($update);
需要注意的地方
worker 模式改变的不只是性能,还有可靠性。PHP-FPM 每 500 个请求回收进程时看不见的内存泄漏,会变成缓慢渗漏,一个周末把某个 worker 拖垮。给每个 worker 设 max_requests 定期回收;监控单 worker 内存;生产环境用 opcache.validate_timestamps=0,靠替换容器部署,而不是原地改文件。
另外,FrankenPHP 不是每一份 Nginx 配置的平替。如果有多年调优的 rewrite 规则、静态资源处理、缓存头,得留出时间移植到 Caddyfile 语法。Caddy 的配置语言熟悉之后很好用,但它不是 Nginx。
现在能用吗
能用,但要睁着眼用。Shopware 已经在生产跑了一年多,Symfony CLI 内置了它,主要 Laravel 托管商现在把它和 Swoole 版 Octane 并列为一级运行时。生态扩展、监控集成、Docker 镜像都成熟了。
如果你跑的是中小型 PHP 应用,想在不重写代码的前提下从现有基础设施里再挤出性能,这周起一个 FrankenPHP 容器,和当前栈做基准对比。最差的结果是学到点东西,最好的结果是删掉三层基础设施。