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

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

✨步子哥 (steper) 2026年03月01日 09:05
想象你开了一家拉面店。 传统 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 把命令写对,空格别丢: ```bash composer require laravel/octane php artisan octane:install ``` 安装时会提示你选择服务器类型。选择 **frankenphp**。然后它会生成配置文件: - `config/octane.php` ## 2)启动(本地开发/验证) ```bash php artisan octane:start --server=frankenphp ``` 到这一步,你的 Laravel 已经跑在 FrankenPHP 的 Worker 模式上了。 迁移成本为什么会让人上瘾?因为很多项目真的就是: - 装个包 - 起个服务 - 访问同样的路由 - 发现“怎么突然这么快?” 这就是“开店方式变了”的威力:你业务代码没动,但你不再每单从毛坯开始装修。 --- # 部署:你可以像传统方式一样稳,也可以像分发单文件那样爽 FrankenPHP 的部署方式很灵活,适合不同团队成熟度: ## 1)生产环境(推荐路线) - 跑 **官方独立二进制文件(standalone binary)** - 或者用 **官方 Docker 镜像** 独立二进制文件这个点非常“邪道但好用”: > 它可以把 **PHP 运行时 + Caddy + 你的应用** 打包成一个可执行文件。 > 你不需要在目标机器上单独装 PHP,也不用纠结“线上 PHP 版本到底是谁装的、和本地差多少”。 这在以下场景特别舒服: - 你想要更强的一致性(“我本地能跑,线上也能跑”更接近现实) - 你要交付给别的团队/客户环境 - 你不想维护一堆服务器上的 PHP 扩展与版本 ## 2)本地开发 你甚至可以继续用熟悉的 Artisan 命令启动,体验很接近原来的开发方式: ```bash php artisan octane:start --server=frankenphp ``` --- # 真正的代价:常驻进程不是“更快的 PHP-FPM”,它是另一种物种 到这里,故事开始反转。 Worker Mode 的世界观是:**进程不会因为一个请求结束就消失**。 这会带来一个传统 PHP 开发者最容易踩的坑——也是最经典的坑: ## 坑王:静态变量 / 全局状态在请求之间“串味” 在传统 PHP 里,你写一个静态计数器,刷新一下就回到 0,你会误以为它“天生安全”。 但在 Worker Mode 里,它会像一口一直没洗的锅:你不刷锅,它就一直带着上一单的味道。 看这个例子: ```php <?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:运行层面设置 “max_requests”,定期重启 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)设置 max_requests + 观测内存曲线 - 你需要知道单个 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 条回复

还没有人回复,快来发表你的看法吧!