您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论

前言:你以为你在优化 PHP,其实你在优化“开店方式”

✨步子哥 (steper) 2026年03月01日 09:05 2 次浏览

想象你开了一家拉面店。

传统 PHP 的工作方式有点像:每来一个客人,你就把店铺从毛坯房开始搭一遍——铺地板、装桌椅、把锅架起来、把汤熬上、把菜单贴墙上。客人吃完走人,你再把店拆掉,下一位客人来了继续从毛坯房开始。

你会说这也太离谱了吧?

但这件事在传统 PHP 的请求模型里,确实每天都在发生。

很多开发者很少甚至完全不去想:我们的代码是怎么从 Git 仓库里的几千个文件,变成用户在浏览器里点一下就能得到响应的“服务”的。我们日常做性能优化,常常盯着 SQL、缓存、索引、队列……这些当然重要,但还有一个体感更直接、也更“底层”的性能开关

你的应用每次请求到底要不要“从零开始把店重建一遍”?
传统上,PHP 生态里我们默认用 Apache / Nginx + PHP-FPM。它稳定、成熟、人人都会;你几乎不用理解太多,就能把 Laravel 跑起来。

但最近 FrankenPHP 像个“会自己长出厨房的店面”一样出现了:部署更顺,很多时候也更快——尤其是当你用到它最让人尖叫的能力:Worker Mode(常驻 Worker)

这篇文章我会用一个“开店”的故事,把它讲清楚,并给你一套能落地的 Laravel 迁移步骤(保留命令与代码块),以及你必须提前知道的坑。


传统 PHP 请求周期:一次请求,一次“重开一家店”

先把“传统 PHP 为什么简单”说透。

传统的 PHP-FPM 模型有一个非常重要的哲学:Share-Nothing(共享无状态)

翻译成人话就是:

  • 每个请求进来时,你的应用几乎像“刚出生”一样干净
  • 请求结束,一切清空(至少在你的心智模型里是这样)
这带来两个超级大的好处:
  1. 心智负担低:你不用担心“上一个请求设置的变量”影响下一个请求
  2. 调试直观:刷新页面就像重启应用(某种意义上真的是)
所以传统 PHP 才能成为“把 Web 开发平民化”的一代神话。

但这套模式的代价也非常直接:每次请求都要把一大堆启动工作重复做一遍

你在 Laravel / Symfony 这种现代框架里,典型会重复发生的事情包括:

  • Composer autoload 触发类加载(虽然有优化,但本质还是启动成本)
  • 读取 .env / 配置文件,解析、合并、缓存(或者检查缓存是否存在)
  • 构建/初始化容器(Service Container)
  • 注册服务提供者(Service Providers)
  • 加载路由、编译中间件链
  • 各种框架“启动期”的绑定、扫描、事件监听器注册……
你可能会说:“这些不是都有缓存吗?route:cache、config:cache 啊。”

对,缓存会让它好一点,但它仍然是“每次请求都要走一遍启动流程”。只是启动流程变短了,而不是消失了。

于是你就会遇到一个很现实的场景:

某些接口的业务逻辑可能只干了 5ms 的事(查一次缓存、拼个 JSON), 但整个请求却要 30ms、50ms、甚至更高。 你优化业务到极致,发现瓶颈不是业务,是“开店”。
结果就是:
  • CPU 大量时间花在框架 bootstrap 上
  • 延迟抬高,P99 更难看
  • 吞吐被“重复启动”卡住
  • 机器钱花得更冤(因为你买的是 CPU 时间,结果 CPU 在重复做热身运动)

这就是应用服务器:别每次都毛坯交付

如果继续用“拉面店”比喻:

  • 传统 PHP:每个客人都要从“建店”开始
  • 应用服务器 / 常驻进程模型:店一直开着,厨具一直热着,汤一直在锅里
客人来了直接下单,做完这一单继续等下一单

这就是很多语言(Node.js、Java、Go、Python 某些部署方式)早就习惯的世界:进程常驻、应用常驻、请求只是进程里的一次函数调用

PHP 不是不能这样做,只是历史上默认选择了“更易用、更安全的心智模型”。而现在,FrankenPHP 把这条路铺得更平了。


FrankenPHP:把 PHP 变成“会常驻的现代应用服务器”

FrankenPHP 走的是一条很有意思的路线:

  • 它基于 Caddy(一个现代 Web Server)
  • Go
  • PHP 运行时嵌进服务器进程里
这和你传统认知里的 “Nginx 转发给 PHP-FPM” 不太一样。你可以把它想成:
“服务器”和“PHP”不再是隔壁两家店,而是同一个店面里:前台接客、后厨做饭、收银结账是一套班子。
它还自带一堆现代 Web 体验的加成(有些来自 Caddy,有些是 FrankenPHP 自己做的):
  1. HTTP/2 / HTTP/3:像把乡村土路换成高架+ETC,连接层面更现代
  2. Early Hints(103):像服务员先上小菜,主菜还在做,但你已经开始吃了(浏览器提前预加载资源)
  3. Graceful Reload:像换厨师不关店,客人不需要“稍后再来”(更容易零宕机滚动)
这些已经很香了,但真正改变游戏规则的是:

Worker Mode:把“启动框架”从每单必做变成“开店只做一次”

在 Worker Mode 下:

  • 第一次启动时:把 Laravel/Symfony 整套框架引导起来,容器建好,路由加载好……
  • 然后:进程常驻,应用常驻
  • 后续请求:跳过绝大多数启动过程,直接用已经热好的“厨房”处理请求
这意味着什么?

意味着你之前每次请求里那段“框架启动耗时”,在 Worker Mode 下几乎可以直接归零(或大幅减少)。很多项目里,性能提升会非常夸张——尤其是那些“业务很轻、框架启动占比很高”的 API。

当然,它不是魔法。它把传统 PHP 的“每次重置世界”换成了“世界一直存在”,性能上去了,但你也要开始面对一个新的课题:

你的代码是否能在常驻进程里保持正确性?
这个我们后面专门讲坑。

Laravel 迁移到 FrankenPHP:像给店装了个常驻厨房(Octane)

Laravel 生态里,想要优雅地进入 Worker Mode,推荐路线是用 Laravel Octane。Octane 本质上就是 Laravel 官方给“常驻 Worker 服务器”做的一层适配与约束,让你不用自己手搓生命周期管理。

1)安装 Octane

把命令写对,空格别丢:

composer require laravel/octane
php artisan octane:install

安装时会提示你选择服务器类型。选择 frankenphp。然后它会生成配置文件:

  • config/octane.php

2)启动(本地开发/验证)

php artisan octane:start --server=frankenphp

到这一步,你的 Laravel 已经跑在 FrankenPHP 的 Worker 模式上了。

迁移成本为什么会让人上瘾?因为很多项目真的就是:

  • 装个包
  • 起个服务
  • 访问同样的路由
  • 发现“怎么突然这么快?”
这就是“开店方式变了”的威力:你业务代码没动,但你不再每单从毛坯开始装修。

部署:你可以像传统方式一样稳,也可以像分发单文件那样爽

FrankenPHP 的部署方式很灵活,适合不同团队成熟度:

1)生产环境(推荐路线)

  • 官方独立二进制文件(standalone binary)
  • 或者用 官方 Docker 镜像
独立二进制文件这个点非常“邪道但好用”:
它可以把 PHP 运行时 + Caddy + 你的应用 打包成一个可执行文件。 你不需要在目标机器上单独装 PHP,也不用纠结“线上 PHP 版本到底是谁装的、和本地差多少”。
这在以下场景特别舒服:
  • 你想要更强的一致性(“我本地能跑,线上也能跑”更接近现实)
  • 你要交付给别的团队/客户环境
  • 你不想维护一堆服务器上的 PHP 扩展与版本

2)本地开发

你甚至可以继续用熟悉的 Artisan 命令启动,体验很接近原来的开发方式:

php artisan octane:start --server=frankenphp

真正的代价:常驻进程不是“更快的 PHP-FPM”,它是另一种物种

到这里,故事开始反转。

Worker Mode 的世界观是:进程不会因为一个请求结束就消失

这会带来一个传统 PHP 开发者最容易踩的坑——也是最经典的坑:

坑王:静态变量 / 全局状态在请求之间“串味”

在传统 PHP 里,你写一个静态计数器,刷新一下就回到 0,你会误以为它“天生安全”。

但在 Worker Mode 里,它会像一口一直没洗的锅:你不刷锅,它就一直带着上一单的味道。

看这个例子:

<?php

class RequestCounter
{
    public static int $count = 0;

    public static function increment(): int
    {
        // 第一次请求:返回 1
        // 第二次请求:返回 2(而不是重新从 1 开始!)
        // 状态在请求之间持续存在
        return ++self::$count;
    }
}

这不是 bug,这是你换了“开店方式”之后的物理规律。

常见“串味”来源清单

有经验的后端同学,我建议你把下面这段当成迁移 checklist:

  • static 属性、全局变量缓存了请求相关数据
  • 单例(Singleton)里存了“上一次请求”的用户信息/租户信息/语言信息
  • 某个第三方 SDK 用静态属性记住了 token、trace id、header
  • 你把 Request/Response 或者容器里某个请求级对象塞进了长期存活的地方
  • 自己写的“简易缓存”没有 TTL/上限,常驻进程里会无限长大
  • 文件句柄、Curl handle、流式资源没释放,慢慢把 worker 拖死
换句话说:
以前 PHP 的“请求结束自动重置世界”像自带洗碗机。 现在你把洗碗机拆了,换来了极速出餐。 但你得学会什么时候洗锅、怎么洗锅、多久换一次锅。

怎么把坑填平:两条路,一个原则

原则:请求级状态必须“每次请求都新鲜”

你可以用两条路线来实现这个原则:

路线 A:编码层面把“进程级”和“请求级”分清楚

  • 进程级:可以缓存纯函数式的结果(例如配置解析后的结构、不可变的映射表、正则编译结果)
  • 请求级:用户信息、权限、租户、语言、Trace、请求参数……必须每次请求重新绑定
避免做这些事:
  • 在静态属性里放 request 相关对象
  • 在单例服务里缓存“当前用户”
  • 在全局容器里偷偷塞 request 变量(然后假装它会消失)

路线 B:运行层面设置 “maxrequests”,定期重启 worker

就算你写得再干净,也建议设置一个合理的上限,让 worker 处理一定数量请求后自动重启,作用类似 PHP-FPM 的“定期换新厨具”:

  • 避免内存碎片/泄漏积累
  • 避免第三方库偶发的状态污染长期存在
  • 让系统具备“自愈能力”
在 Octane 里你可以在 config/octane.php 里配置类似参数(不同版本配置项可能略有差异,但核心思想一致):限制单个 worker 的请求处理次数

你可以把它理解成:

“这口锅用 5000 单就换一口,别省这点钱。”

迁移后的“性能预期管理”:快在哪里,可能不快在哪里

有经验的后端读者通常最关心这句:到底提升来自哪里?

快的部分(通常很明显)

  • 框架 bootstrap 少了很多
  • 容器构建、Provider 注册、路由加载等重复劳动减少
  • 对“业务轻、请求多”的 API 特别友好

未必快的部分(别把锅甩错)

  • 你数据库慢:Worker 模式救不了慢 SQL
  • 你外部 API 慢:Worker 模式只能让你“更快地开始等待”
  • 你 IO 模型/网络瓶颈:需要另一个层面的优化
我的建议是:迁移前后都做一次对比压测,关注两件事:
  1. 吞吐(RPS):单位时间能处理多少请求
  2. 尾延迟(P95/P99):用户体感通常死在尾部
Worker 模式往往对尾延迟也有帮助,因为启动抖动少了,但如果你有锁竞争、慢查询、GC/内存膨胀等问题,尾延迟可能仍然会咬你。

生产落地建议:把“开店指南”贴在墙上

给你一套更偏工程落地的建议,方便你真的把 FrankenPHP/Octane 放进生产环境,而不是跑个 demo 就结束。

1)把“无状态”当成团队规范,而不是个人习惯

  • Code Review 里专门看:有没有静态变量缓存请求信息
  • 约定:用户态信息只能存在于 request scope(请求生命周期内)
  • 对第三方 SDK 做隔离层,别让它的内部静态状态污染你的核心域

2)设置 maxrequests + 观测内存曲线

  • 你需要知道单个 worker 的内存是否随请求数上升
  • 如果上升:要么你有泄漏,要么你缓存无上限,要么第三方库在囤东西
  • 先用 max_requests 做保险,再慢慢定位根因

3)区分“缓存”与“记忆”

在常驻进程里,缓存是一把刀,用得好很爽,用不好很吓人。

  • 缓存应该有:上限、TTL、淘汰策略
  • “记忆”(把请求上下文记住)通常是灾难

4)部署与回滚:优雅重载只是基础,回滚策略才是底气

Graceful reload 能让你更容易无宕机更新,但你依然要准备:

  • 一键回滚(镜像/二进制版本)
  • 健康检查
  • 灰度发布(先一小部分流量验证)
  • 指标监控(错误率、延迟、内存、worker 重启频率)

你到底该不该用:一句人话判断

如果你的服务满足下面任意几条,FrankenPHP Worker Mode 通常很值得试:

  • API 请求量大,业务逻辑相对轻(框架启动占比高)
  • 你在乎尾延迟(P99)与吞吐
  • 你不想折腾复杂的运行时拼装,想要更“一体化”的部署体验
  • 你愿意为性能换一点点“长驻进程心智模型”的学习成本
反过来,如果你的团队非常依赖“刷新即重置世界”的直觉、代码里大量使用静态/单例保存请求态、又没有足够的测试与观测,那么上 Worker 之前,建议先把基础工程化补齐一点,不然你会体验到另一种刺激:性能是快了,但偶发 bug 像鬼故事。

总结:从“每单开店”到“店一直开着”

把全文压成 5 句话:

  1. 传统 PHP 的 Share-Nothing 很省心,但代价是每次请求都要重复框架启动
  2. 在现代框架里,这段启动成本可能比你业务逻辑还贵
  3. FrankenPHP 的 Worker Mode 让应用常驻内存,把“启动开销”从每次请求的必选项变成一次性成本
  4. Laravel 用 Octane 接入 FrankenPHP 非常顺滑:装包、安装、启动即可
  5. 常驻进程会带来“请求间状态泄漏”等新坑;通过编码约束 + max_requests 定期重启 + 监控,可以把风险控制住
最后一句个人感受是:一旦你习惯了“厨房一直热着”的世界,再回到“每单从毛坯装修”的模式,会很难适应。 有条件的话,真心建议你拿一个真实项目(最好是轻业务 API)试一轮压测,你会立刻知道它值不值。

讨论回复

0 条回复

还没有人回复