Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

FrankenPHP 1.4 + Laravel 12 Worker 模式生产实战:3 倍吞吐背后的状态隔离与迁移指南

TL;DR:FrankenPHP 1.4(2026 Q1 发布)通过 Caddy 2.8 + 嵌入 PHP + Octane 兼容 worker 协议,在四核盒子上把 Laravel 12 仪表盘端点从 PHP-FPM 的 220 req/s 提到 690 req/s——三台机器做一台的活儿,月费从 $740 砍到 $310。但 3 倍吞吐的代价是应用代码必须 worker-safe:静态属性、singleton、容器单例都会跨请求残留。本文从架构、基准、踩坑到完整迁移清单一次性讲透。

一、为什么 2026 是 FrankenPHP 的"生产时刻"

FrankenPHP 并不是 2026 年才出现——它由 API Platform 作者 Kévin Dunglas 在 2022 年首次发布,核心思路是把 PHP 解释器嵌入到 Go 写的 Caddy 服务器中,做成一个独立的 PHP 应用服务器二进制。听起来像"另一个 RoadRunner/Swoole",但过去四年它一直停留在"小众玩具"的位置。

2026 年,两件事同时到位

  1. Caddy 2.8 收尾了运维欠账:graceful reload 终于稳定、HTTP/3 在生产环境不再丢包、配置 DSL 不再因热重载丢连接。
  2. Laravel 12.x 在 Application 核心类里合入了 worker-safety 提示(12.2 版本),直接为长生命周期 worker 运行时做适配——这是 Octane 同款 wiring,但底层从 Swoole/RoadRunner 换成了 Caddy。

顺带说一句:FrankenPHP 已正式从 Kévin 个人项目转入 php/frankenphp(PHP 官方 GitHub 组织),vendor lock-in 风险大幅降低。

这意味着 2026 年对 Laravel 团队来说,从 PHP-FPM 迁移到 FrankenPHP 不再是"追新潮",而是"少交三台机器的钱"。

二、架构速览:Caddy + 嵌入 PHP + Octane 协议

                    ┌─────────────────────────────────────────┐
                    │  Caddy 2.8 Web Server (Go 实现)         │
   HTTP/1.1         │  ┌──────────────────────────────────┐  │
   HTTP/2  ────────►│  │  PHP Worker Pool (CGI 协议)    │  │
   HTTP/3           │  │  ┌─────┐ ┌─────┐ ┌─────┐ ┌────┐ │  │
                    │  │  │ W1  │ │ W2  │ │ W3  │ │ W4 │ │  │
                    │  │  └─────┘ └─────┘ └─────┘ └────┘ │  │
                    │  │   ↑ 静态文件/TLS/限流/日志都在    │  │
                    │  │     Caddy 层完成,worker 只跑 PHP │  │
                    │  └──────────────────────────────────┘  │
                    └─────────────────────────────────────────┘

三个关键点

  • 单二进制:1 个 frankenphp 可执行文件 = Caddy + PHP 解释器 + worker 运行时,部署简化到极致。
  • HTTP server 与 PHP worker 共享同一个进程:Caddy 处理 TLS、HTTP/3、限流、静态文件,只有真正需要执行 PHP 的请求才路由给 worker。
  • Octane 协议复用:Laravel 代码用 Octane::serve() 或对应的 worker 入口,与 Swoole/RoadRunner 体验一致。

对比 Octane-Swoole:Octane 只是一层运行时适配,你仍然需要 Nginx 在前面处理 TLS。FrankenPHP 把整个栈压成一个进程——对没有专职运维的小团队,这是比"快 10%"更重要的胜利。

三、实测基准:四核盒子上 690 req/s

测试方法:四核 CPU/8GB 内存,Laravel 12.2,应用是一个仪表盘端点(2 次 MySQL 查询 + 1 次 Redis 读取),wrk 30 秒压测,wrk 脚本相同。

运行时相对吞吐冷启动HTTPS / HTTP3Worker 模式运维复杂度
Nginx + PHP-FPM(基线)1.0x(≈220 req/s)~20 ms需 Nginx 配置2 进程、2 份配置
Laravel Octane + Swoole 6.03.1x~3 ms需前置代理Swoole 扩展 + 代理
Laravel Octane + RoadRunner2.8x~4 ms需前置代理Go 二进制 + 代理
FrankenPHP 1.4(worker)3.0x(≈690 req/s)~3 ms内置单二进制

数据来源:datasofttechnologies 实测 与社区基准。

翻译成钱:一个中型物流仪表盘原本需要 3 台 EC2 在业务时段跑,自动伸缩组月费 $740;切到 FrankenPHP worker 后,单台留 30% 余量就能扛,月费降到 $310。

注意:"3 倍"是有边界的——纯 CPU 计算型业务(重计算、无 IO)差距不明显;真正享受红利的是 web/IO 密集型 SaaS(Laravel 默认业务形态)。

四、5 分钟上手:Laravel 12 + FrankenPHP

4.1 安装运行时

bash
# macOS / Linux 一行装好
curl -fsSL https://frankenphp.dev/install.sh | bash
mv frankenphp /usr/local/bin/

# 验证
frankenphp version
# frankenphp v1.4.2 (with Caddy 2.8.x, PHP 8.3.x)

4.2 Laravel 12 项目接入

bash
# 在现有 Laravel 12 项目根目录
composer require laravel/octane
php artisan octane:install --server=frankenphp

这会生成 octane:frankenphp 启动命令和一个 frankenphp/Caddyfile(在项目根)。

4.3 写第一个 Caddyfile

caddyfile
{
    frankenphp
    order php_server before file_server
}

localhost {
    root * public/
    php_server {
        worker /usr/local/bin/php artisan octane:frankenphp --max-requests=500
        env APP_ENV production
        env APP_DEBUG false
    }
}
  • worker 指定 worker 入口命令
  • --max-requests=500 表示每个 worker 处理 500 个请求后自动回收(防止内存泄漏累积)

4.4 启动 & 验证

bash
# 开发模式
frankenphp run --config ./frankenphp/Caddyfile

# 生产模式(Supervisor/systemd 管)
frankenphp run --config ./frankenphp/Caddyfile --env-file .env

打开 http://localhost:8000,看看 dashboard——是快了还是出错了,要从这一行命令开始观察。

五、Worker 模式的三个致命陷阱(实战踩雷)

FrankenPHP worker 模式的"3 倍吞吐"不是免费的——Laravel 启动一次后进程会存活数千个请求,任何"每次请求重置"的隐式假设都会翻车。

5.1 静态属性 / singleton 跨请求残留

php
// ❌ 致命:所有请求共享同一个 $tenantId
class TenantContext
{
    public static ?string $tenantId = null;

    public static function set(string $id): void
    {
        self::$tenantId = $id;
    }
}

// 请求 A:TenantContext::set('tenant-A')
// 请求 B:TenantContext::set('tenant-B')
// 2000 个请求后,dump 出来发现 self::$tenantId 还是 tenant-B
// 但租户 A 的请求来了——它读到 tenant-B 的值

修复

php
// ✅ 方案 1:改用 Request 作用域(注入 Request 对象)
class TenantResolver
{
    public function resolve(Request $request): string
    {
        return $request->attributes->get('tenant_id')
            ?? abort(403, 'Tenant not resolved');
    }
}

// ✅ 方案 2:写一个 Octane RequestCatcher
// 在 app/Octane/ResetState.php 中
public function bootstrapReceived(Request $request): void
{
    // 每次请求开始前清空自定义状态
    Container::forgetInstance(TenantContext::class);
}

5.2 内存泄漏累积

PHP-FPM 的好处:每个请求结束后进程死掉重起,控制器里漏 2 MB 不在乎。Worker 模式:进程活着——2 MB 漏一次,1000 个请求后就吃掉 2 GB,进程开始 OOM。

实战守则

php
// ✅ 1) worker 启动数量匹配内存(不是越多越好)
// 4GB 机器上每个 worker 大约 80-150 MB → 开 8-12 个 worker

// ✅ 2) 强制 worker 周期性回收
// Caddyfile: worker /usr/local/bin/php artisan octane:frankenphp --max-requests=500

// ✅ 3) 部署监控:worker 内存超过阈值就告警
// Prometheus + Grafana 抓 caddy 指标

5.3 全局单例的隐式副作用

php
// ❌ 老 SDK 把单例挂到容器根
class LegacySentryClient
{
    public function __construct()
    {
        // 一些 SDK 在构造时就订阅了全局事件
        Event::listen('kernel.handled', [$this, 'flush']);
    }
}

// 第一次启动:注册一次监听器
// 1000 个请求后:监听器被注册 1000 次 → 内存爆炸 + 行为错误

修复

php
// ✅ 用 app()->scoped() 显式声明单次请求作用域
class LegacySentryClient
{
    public function __construct()
    {
        // 移到一个 request-scoped 服务中
    }
}

// 在 AppServiceProvider 里
$this->app->scoped(LegacySentryClient::class, function () {
    return new LegacySentryClient();
});

作者注:Laravel 11+ 的容器新增了 scoped() 方法,专为这种"单次请求生命周期"的场景设计——这是 2026 年所有迁移到 worker 模式的 Laravel 项目的必用 API。

六、生产部署清单(2026 实操版)

6.1 镜像

dockerfile
# 多阶段构建,最终镜像只有运行时
FROM dunglas/frankenphp:1.4.2-php8.3 AS base

# 生产镜像(极简)
FROM dunglas/frankenphp:1.4.2-php8.3
WORKDIR /app
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY . .
RUN composer install --no-dev --optimize-autoloader
RUN php artisan config:cache && php artisan route:cache && php artisan view:cache

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost/health || exit 1

EXPOSE 80 443
CMD ["frankenphp", "run", "--config", "/app/frankenphp/Caddyfile"]

镜像只有 130 MB 左右——比标准 php:8.3-fpm + nginx 复合镜像小一半。

6.2 反向代理(可选)

如果需要在前面再叠一层(边缘 LB、企业网关、Cloudflare),记得:

nginx
# Nginx 配置要点
location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Connection "";          # ⚠️ 关键:关闭 keepalive,避免 worker 阻塞
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 60s;
}

这个 Connection: "" 是个老坑——不关 keepalive,前端连接被复用,worker 会"卡死"在等下一个请求上。

6.3 K8s 部署(Deployment + HPA)

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: laravel-frankenphp
spec:
  replicas: 3
  selector:
    matchLabels: { app: laravel }
  template:
    metadata:
      labels: { app: laravel }
    spec:
      containers:
      - name: app
        image: registry.example.com/laravel-frankenphp:1.4.2
        ports:
        - containerPort: 80
        - containerPort: 443
        resources:
          requests: { cpu: "500m", memory: "512Mi" }
          limits:   { cpu: "2",    memory: "1Gi" }
        livenessProbe:
          httpGet: { path: /health, port: 80 }
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet: { path: /ready, port: 80 }
          periodSeconds: 5
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: laravel-frankenphp
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: laravel-frankenphp
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target: { type: Utilization, averageUtilization: 70 }

6.4 监控 & 告警

指标工具告警阈值
Worker 内存Prometheus + Grafana单 worker > 256 MB 持续 5 分钟
请求延迟 P99OpenTelemetry → Grafana Tempo> 800ms
HTTP 5xx 比例Caddy 自带 metrics> 0.5%
Worker 重启次数日志聚合每分钟 > 3 次(说明 max-requests 太低)

七、FrankenPHP vs Octane:到底怎么选

维度FrankenPHP 1.4Laravel Octane(Swoole 6.0)
性能(web/IO 密集)3.0x3.1x
性能(CPU 密集)2.5x3.5x
HTTPS/HTTP3内置需前置代理
部署复杂度单二进制Swoole 扩展 + 代理
协议层Octane worker 协议Octane worker 协议
现有 Octane 知识复用
PHP 扩展依赖极少需 Swoole 编译
切换成本(已是 Octane 协议)

结论

  • 新项目 / 现 PHP-FPM 项目:直接上 FrankenPHP 1.4,省一台机器的钱+省一个 Nginx 配置文件。
  • 已稳定跑 Swoole/RoadRunner 的项目:没必要迁移,差距不到 5%,迁移收益不抵风险。
  • PHP 版本 < 8.3 的老项目:先升 PHP,再迁 FrankenPHP。

八、迁移路线图(4 步走,2 周搞定)

Step 1:兼容审计(第 1-3 天)

bash
# 启动 worker 模式跑测试
FRANKENPHP_WORKER=1 php artisan test

# 跑一周
php artisan octane:start --server=frankenphp
# 7 天后查日志:有没有 OOM?有没有跨请求状态泄漏?

Step 2:灰度上线(第 4-7 天)

nginx
# Nginx 在 health 端点切到 FrankenPHP,其他路由走 PHP-FPM
location /health {
    proxy_pass http://127.0.0.1:8001;  # FrankenPHP
}
location / {
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;  # 老的 PHP-FPM
}

观察 P99 延迟、内存、5xx 比例。

Step 3:路由切流(第 8-10 天)

nginx
# 切 10% 流量
split_clients $request_id $frankenphp_pool {
    10%     127.0.0.1:8001;
    *       unix:/run/php/php8.3-fpm.sock;
}

逐步 25% → 50% → 100%。

Step 4:下线 PHP-FPM(第 11-14 天)

关停 PHP-FPM,监控一周——稳定后正式切换。

九、结语:Laravel 生态的一次静默革命

FrankenPHP 1.4 在 2026 年成为 Laravel 团队的"默认生产选项",不是一次惊天动地的革命,而是无数个 30%、3%、30% 累积出来的

  • Caddy 2.8 让运维欠账清零
  • PHP 8.5 让 JIT/Fiber 都到位
  • Laravel 12 把 worker-safety 写入核心
  • FrankenPHP 1.4 把协议层、HTTP 层、进程层合成一个二进制

你不需要再争论"Octane 值不值"——这套组合的 ROI 在大多数 web 业务上 6 个月内回本。剩下的就是按清单迁移,把"3 台机器做 1 台机器的活儿"变成真金白银。

如果你正在评估,下周找个空闲的下午,跑一次 wrk 压测——数据会替你做决定。

参考资料

上次更新于: