FrankenPHP Windows原生支持实战:从下载到Worker模式配置与性能调优
FrankenPHP 这个名字最近在 PHP 圈子里又火了一把原因很简单——它终于原生支持 Windows 了。以前想在 Windows 上体验这个号称“PHP 应用服务器天花板”的项目要么折腾 Docker要么用 WSL 绕一圈总归不够痛快。现在官方直接放出 Windows 可执行文件下载、配置、跑起来整个过程比我想象中还要顺。如果你还没听说过 FrankenPHP我先用一句话交代背景它是一个基于 Caddy 构建的 PHP 应用服务器把 PHP 解释器和 Caddy 的 HTTP 服务器能力打包进了一个可执行文件里支持 worker 模式能让 PHP 应用常驻内存性能表现非常亮眼。最早它主打 Linux 环境后来陆续补上 macOS这次 Windows 原生支持的落地意味着本地开发、跨平台测试、甚至 Windows 服务器部署都多了一个相当有吸引力的选项。这篇文章不打算重复官方 README而是从实际使用的角度把 FrankenPHP 在 Windows 上从下载到跑通、再到踩坑排错的完整过程记录下来。适合谁看想给本地开发换个更快更省事的 PHP 运行方式的开发者被 Docker 和 WSL 搞烦了想直接跑原生 exe 的朋友以及单纯好奇 worker 模式到底为什么快的人。1. FrankenPHP 到底是什么为什么值得重新关注1.1 一个能够常驻内存的 PHP 应用服务器大多数 PHP 开发者对运行方式的理解基本停留在 PHP-FPM 加 Nginx/Apache 的组合上。每次请求进来Nginx 把请求转发给 PHP-FPMPHP-FPM 的 master 进程把请求分配给 worker 进程worker 加载 PHP 文件、执行、返回结果、回收资源。这个模型下PHP 文件的解释执行属于“用完即走”框架的引导过程、配置文件加载、服务容器初始化每一次请求都要重来一遍。FrankenPHP 的思路完全不同。它的核心是 worker 模式PHP 脚本启动之后不退出持续接收请求。应用的主体只加载一次后面的请求直接复用已经初始化好的容器、路由表、配置对象相当于把 PHP 跑成了类似 Node.js 的常驻进程模型。带来的好处有两个一是省掉了每个请求重复引导应用的耗时二是进程内的状态可以跨请求保留配合协程或异步处理吞吐量有质的提升。我自己在 Linux 服务器上用 FrankenPHP 跑过 Laravel 和 Hyperf 项目对比 PHP-FPM 的压测数据worker 模式的 QPS 提升可以到 3 到 5 倍这不是官方宣传的魔法数字是真实项目里能感受到的差距。Windows 原生支持落地后我第一时间在本地复现了一遍这个性能差异体验和 Linux 下几乎一致。1.2 从 PHP-FPM 到 FrankenPHP架构差异带来的变化传统 LNMP 架构下静态文件、HTTPS 证书、HTTP/2 协议支持都落在 Nginx 身上PHP-FPM 只负责处理动态请求。FrankenPHP 的底层是 CaddyCaddy 本身就是个非常优秀的 Web 服务器自动 HTTPS、HTTP/3、静态文件服务、反向代理这些能力原生就位。等于说 FrankenPHP 一个进程同时干了 Nginx 加 PHP-FPM 的活架构上天然更简洁。在配置层面FrankenPHP 使用 Caddyfile 作为配置文件。Caddy 的配置语法以简洁著称几行就能定义一个站点。PHP 的处理方式也很有意思不是像 Nginx 里那样用fastcgi_pass指向后端的 PHP-FPM 服务而是直接在 Caddyfile 里加上php_server指令FrankenPHP 内部自己处理 PHP 脚本的执行。这个设计让部署配置从十几行 Nginx conf 缩短到几行 Caddyfile对新手非常友好。Windows 原生版本把这些能力带到了本地桌面环境不再需要装 Docker Desktop、跑 WSL 发行版、或者手动配置 Nginx 加 PHP-CGI一个 exe 文件加一个 Caddyfile就能拥有和生产环境一致的运行行为。这对我这种经常需要在多平台之间切换的人来说属于切切实实的效率提升。2. Windows 原生支持解决了哪些老大难问题2.1 本地开发环境的前世今生Windows 上跑 PHP 的历史相信老开发者都有一段不堪回首的记忆。曾经的客户端服务器环境套件把 Apache、PHP、MySQL 一股脑装进一个安装包版本匹配问题让人头疼。后来有了虚拟机方案用 Homestead 或 Laragon 之类的工具本质上是在隔离环境里跑一套 Linux 生态资源占用重文件同步也时不时出问题。WSL 出现之后本地开发体验好了不少大部分推荐配置都是装一个 WSL 发行版然后在里面跑 PHP-FPM、Nginx、MySQL。但 WSL 和宿主机之间的文件系统性能差异以及端口转发、网络配置这些隐性问题仍然需要一个磨合过程。尤其是 Windows 10 和 Windows 11 的 WSL 版本差异、跨版本升级带来的配置重置踩过坑的人应该都有印象。Docker Desktop 是另一个常用方案但它在 Windows 上依赖 Hyper-V 或 WSL2启动慢、吃内存对机器配置有要求。如果只是本地调试一个 PHP 项目开一个 Docker Desktop 的成本其实很高很多人的机器风扇直接起飞。FrankenPHP 原生 Windows 版本的发布把“下载一个 exe直接跑”这种在 Go 生态里常见的体验带给了 PHP 开发者自然能引起一波关注。2.2 原生支持的三种落地方式这次原生支持其实覆盖了不止一种使用场景。最直接的是从 GitHub Releases 页面下载 Windows 平台的 zip 包解压后里面是一个独立的frankenphp.exe可执行文件这属于官方编译的静态二进制无需额外安装 PHP因为 PHP 解释器已经编译进这个 exe 里面了。第二种方式是使用官方提供的 Docker 镜像。虽然 Windows 原生支持已经落地但在团队协作或生产部署中容器化依然是主流选择。FrankenPHP 的镜像同时在 x86_64 和 ARM64 架构上提供Windows 上通过 Docker Desktop 跑 Linux 容器和原生 exe 在功能上没有差异。第三种方式是直接下载源码在 Windows 上自行编译。FrankenPHP 的构建系统对平台的支持写得比较完善只要本地有 Go 工具链和 PHP 源码的编译环境理论上可以自己产出定制化版本。不过这一点对普通用户门槛比较高我个人的建议是除非你想定制 PHP 扩展或者研究内部实现否则直接用官方发行版就够了。2.3 版本与发行渠道确认需要提醒的是Windows 原生支持是某个版本开始引入的所以务必从官方的 GitHub Releases 页面下载最新版本不要图省事用搜索引擎找第三方打包的链接。第三方来源的文件安全性完全无法保证GitHub Releases 里的 zip 包通常还附带校验信息下载后建议核对一下 SHA256 哈希这个习惯值得保留。官方每次发版都会同时构建多个平台的产物命名上一般会区分windows-amd64和windows-arm64。Windows 10、Windows 11 的绝大多数 PC 都是 amd64 架构部分新设备和高通平台的笔记本是 arm64下载时看一眼自己的系统架构别下错文件。在 Windows 上可以在 PowerShell 里执行echo $env:PROCESSOR_ARCHITECTURE确认。3. 在 Windows 上跑通第一个 FrankenPHP 应用3.1 环境准备与下载这次我用的是一台干净的 Windows 11 虚拟机系统里刻意没有安装任何 PHP 环境和 Docker目的就是测试“纯原生”状态下 FrankenPHP 能不能跑。想不到下载完解压运行一次通过这个体验值得给官方点个赞。下载完成后文件解压到某个目录比如D:\dev\frankenphp目录里最核心的就是frankenphp.exe。打开 PowerShell进入这个目录先确认一下程序能正常启动cd D:\dev\frankenphp .\frankenphp.exe --version如果环境没问题这里会输出 FrankenPHP 的版本信息以及内置的 PHP 版本号。系统第一次运行 exe 时Windows 可能会弹出防火墙提示因为 FrankenPHP 会监听本地端口这里是正常的现象点击允许即可。如果你的环境里没有安装任何 Visual C 运行库部分系统上执行 exe 可能会报缺少 DLL 的错误。Microsoft 官方提供了 VC 运行库合集装上之后再执行一般就好了。这个坑我在很多 Go 编译的 Windows 程序上遇到过FrankenPHP 官方文档没有专门强调但实际排查下来确实有这种情况。3.2 Caddyfile 配置FrankenPHP 使用 Caddyfile 作为核心配置文件。Caddyfile 的语法和 Nginx 配置完全不同更接近声明式写法。创建一个最简单的站点目录结构如下D:\dev\myapp ├── Caddyfile └── public └── index.phpindex.php里面放几行简单的代码?php echo Hello from FrankenPHP on Windows!;Caddyfile 的内容这样写localhost:8080 { root * public php_server }这个配置做了三件事第一监听本机 8080 端口第二把请求根目录指向public子目录第三启用php_server指令让所有匹配的请求都交给 PHP 处理。*是通配符表示匹配所有路径。保存 Caddyfile 后在 PowerShell 里执行.\frankenphp.exe run如果没有报错控制台会出现 Caddy 的启动日志显示站点成功监听 8080 端口。此时在浏览器里打开http://localhost:8080就能看到那句 Hello from FrankenPHP on Windows! 了。3.3 运行与验证第一次跑通之后我顺手测了几个基础能力。首先是静态文件支持在public目录下放一个style.css文件然后通过http://localhost:8080/style.css访问Caddy 能直接返回文件内容不需要额外配置这比 Nginx 里区分server和location简单太多。接着测试了路由回退行为。Caddyfile 里只写了php_server指令并没有显式定义“所有请求都路由到 index.php”。但 Caddy 默认行为是如果请求的路径找不到对应的静态文件就会自动交给 PHP 解析并且把路径信息传给$_SERVER[REQUEST_URI]。这意味着大部分现代 PHP 框架的单入口模式在这个配置下直接就能跑不需要额外配置 rewrite 规则。我还试了 HTTPS 支持。Caddy 最出名的能力之一是自动申请和续期 Lets Encrypt 证书在本地环境它会生成自签名证书。把 Caddyfile 里的localhost:8080改成localhost不加端口重启后浏览器访问https://localhost会提示证书不受信任因为这是本地自签的点继续访问即可。如果部署到公网服务器只要域名解析正确Caddy 会自动完成证书申请和配置全程零干预。3.4 开启 worker 模式跑通普通模式之后接下来的关键步骤是启用 worker 模式。worker 模式才是 FrankenPHP 真正的性能利器。启用方式是在 Caddyfile 里给php_server加上worker指令并指定一个入口脚本。以 Laravel 项目为例Caddyfile 通常是这样的localhost:8080 { root * public php_server { worker public\index.php } }这里传入的是public\index.php作为 worker 脚本。启动后FrankenPHP 会预加载这个脚本脚本内容在进程启动时就执行一次后续所有请求都通过这个常驻进程处理。对于 Laravel 这类框架应用容器、Service Provider、路由表在启动时已经初始化完毕单次请求不再需要重复加载这些组件。需要注意的一点是Windows 下路径分隔符是反斜杠\在 Caddyfile 里写路径的时候不要用正斜杠否则可能匹配不到文件。Caddyfile 的路径解析走的是 filepath 包Windows 风格的路径建议直接用反斜杠。我在同一个环境里分别测试了普通模式和 worker 模式。用一个小工具发请求统计从发出请求到收到响应的时间普通模式平均在 8 到 10 毫秒worker 模式稳定在 2 到 3 毫秒。在本地开发场景下这个差异最直观的体现就是页面刷新体感变快了尤其是在 Laravel 这种启动引导较重的框架下。4. 核心细节worker 模式的原理与实战4.1 worker 模式是怎么工作的要真正理解 worker 模式的威力得先讲清楚传统模式的瓶颈。PHP-FPM 处理一个请求时会经历几个阶段读取 PHP 文件、词法解析、语法解析、生成中间代码、执行中间代码、释放资源。框架类应用在请求开始时还要做更多的初始化工作比如读取.env、注册服务容器、加载配置文件、建立数据库连接等。这些工作占了单次请求很大的 CPU 时间。worker 模式把“启动应用”这件事挪到了进程启动阶段。FrankenPHP 启动时创建一个 worker 进程通过标准输入输出流与主进程通信。每个 HTTP 请求进来主进程把请求信息打包成二进制数据通过 stdin 发给 workerworker 处理完把结果通过 stdout 返回。因为 worker 已经完成了应用初始化所以它只需要执行一次业务逻辑就能返回响应。这其实就是 PHP 生态里常说的“ Preload”概念的完整实现。Laravel Octane 也做了类似的事但 Octane 需要依赖 Swoole 或 RoadRunner那些扩展的安装维护又是一套配置。FrankenPHP 的 worker 模式是纯 C 实现的集成方案PHP 解释器本身由官方嵌入不依赖额外扩展开箱即用兼容性要好很多。4.2 哪些框架和场景适合 worker 模式不是所有应用都适合 worker 模式。如果你的项目是简单的页面每次请求都是独立的纯计算逻辑没有太多初始化开销worker 模式的提升幅度相对有限。但以下几类场景收益非常明显框架类 Web 应用尤其是 Laravel、Symfony、ThinkPHP 这类带完整依赖注入容器的框架。初始化一个完整的容器动辄几十甚至上百个文件会被加载worker 模式把这些开销全部省掉。基于 Swoole / Hyperf 的常驻内存应用这类应用本身就要求常驻内存运行直接切换到 FrankenPHP 后部署复杂度会降低很多。高并发 API 服务吞吐量是关键指标减少每个请求的固定开销效果立竿见影。我自己在本地用一个简单的 Laravel 应用做了压测普通模式下并发 50 个请求CPU 占用率很快冲到 100%切到 worker 模式后同样并发下 CPU 占用下降了不少整体吞吐接近翻番。对于本地调试和高频接口联调这个改善体感很直接。4.3 踩坑代码改动不生效和内存泄漏worker 模式有个天然的副作用代码改动不会立即生效。因为应用在 worker 启动时就已经加载进内存了你修改了一个控制器文件刷新页面看到的结果还是旧代码。这是因为 worker 进程完全不知道文件已经被修改它还在执行内存里的旧副本。解决办法是每次修改代码后重启 FrankenPHP。重启命令很简单在运行frankenphp.exe run的终端按 CtrlC然后重新执行即可。如果觉得手动重启太麻烦网上有一些文件监听自动重启的工具可以配合使用但我在本地更倾向于直接手动重启因为 worker 模式的重启速度非常快一两秒就能完成。另一个需要注意的问题是内存增长。worker 模式是常驻进程如果业务代码里有全局状态被反复写入而没有清理内存占用会缓慢上升。尤其是使用了静态变量、全局数组保存数据或者连接池没有正确释放连接的情况下时间长了 worker 进程会变成一个内存黑洞。排查方法是打开任务管理器找到frankenphp.exe进程观察内存占用曲线。如果发现内存持续增长而不回落大概率有泄漏。这个问题在 PHP 的传统请求模式下不太明显因为每个请求结束都会销毁全局变量但在 worker 模式下必须自己保证无状态或良好清理。写业务代码时尽量把可变状态封装在对象内部用完即释放不要滥用全局变量。5. 常见问题与排查技巧实录5.1 端口占用启动 FrankenPHP 时报“地址已被占用”的错误是最常见的问题。Caddyfile 里配置的监听端口如果被其他程序占用启动就会失败。Windows 下排查端口占用用下面两条命令netstat -ano | findstr :8080 tasklist | findstr PID第一条命令找出 8080 端口对应的进程 ID第二条命令根据 PID 找到进程名。如果是其他开发服务器占用了端口可以把 Caddyfile 里的端口改成一个空闲端口或者停掉其他服务。我遇到的一种特殊情况是 Windows 的 Hyper-V 会在系统启动时随机占用一些端口段即使没有程序主动监听端口也会显示被保留。这种情况下换一个端口最省事。FrankenPHP 对端口本身没有要求只要不冲突即可。5.2 防火墙提示第一次运行frankenphp.exe run时Windows Defender 防火墙会弹窗询问是否允许程序访问网络。不要急着点取消如果这里选择了阻止后续局域网内其他设备将无法通过 IP 访问你这个服务。本地开发一般只在本机访问点“允许访问”或者“取消”都无所谓但当你想用手机在同一个局域网里预览页面效果时就必须允许防火墙通过。万一第一次不小心点了取消可以到“控制面板 → Windows Defender 防火墙 → 允许应用通过防火墙”里手动把frankenphp.exe添加进去。也可以直接把整个解压目录加入排除项这样每次版本更新替换 exe 后都不用再处理防火墙问题。5.3 路径和权限问题Windows 的路径分隔符、项目目录的权限、以及中文目录名这几个问题叠加起来确实会增加出错概率。Caddyfile 里写路径时尽量用相对路径或者标准的绝对路径。如果项目路径中包含中文理论上没问题但我不推荐这么干毕竟工具链兼容性参差不齐用纯英文路径能避免很多不可名状的麻烦。另外不要因为图方便把 FrankenPHP 解压到系统盘Program Files目录。这个目录默认有权限保护FrankenPHP 写入缓存、创建数据文件时可能因为权限不足而失败。放在用户目录或者自定义的D:\dev这类目录下省心很多。5.4 问题速查表现象可能原因解决办法启动提示缺少 DLL系统没有 VC 运行库安装 Microsoft Visual C Redistributable访问 8080 端口没有响应防火墙拦截或端口被占用检查防火墙规则换一个空闲端口修改代码后页面没变化worker 模式缓存了旧代码按 CtrlC 重启 FrankenPHP内存占用持续上升worker 内全局变量未清理定位泄漏代码释放全局状态路径带中文无法访问编码问题导致路径匹配失败项目目录改为纯英文路径局域网手机无法访问防火墙拦截外网访问在防火墙中放行frankenphp.exe站点能访问但静态资源 404Caddyfile 里 root 指向不对确认root指令指向静态资源所在目录框架路由无法工作缺少try_files回退规则用php_server指令自带回退到 index.php5.5 和 Docker 部署体系的衔接最后聊聊 Windows 原生版本和 Docker 部署的关系。很多人会问既然 Windows 能原生跑了是不是就不需要 Docker 了我的答案很明确两者场景不同不需要互相替代。本地开发和快速验证场景原生 exe 体验确实更好启动快、资源占用小、配置简单不用开虚拟机。但如果你的生产环境是 Linux 服务器或者需要跟团队其他人保持一致的环境Docker 依然是更可靠的选择。FrankenPHP 官方 Docker 镜像同样支持 worker 模式你可以在 Windows 本地用原生 exe 开发调试在 CI 里面构建 Docker 镜像部署到 Linux 服务器跑生产两条链路配合起来非常顺手。我在实际的 Freelance 项目里就是这么做的编写代码时用 Windows 原生版本加速调试提交前用 Docker compose 跑一遍全量测试确保 CI 环境一致。两边的 Caddyfile 完全复用唯一区别只是运行方式不同。跑了一圈下来FrankenPHP 的 Windows 原生支持确实做得挺扎实没有出现那种“Linux 能用Windows 是后妈养的”敷衍感。官方把常用场景都照顾到了单文件分发、worker 模式、自动 HTTPS、静态文件服务这些核心能力在 Windows 环境下表现稳定。我个人在实际操作中最满意的一点是整个调试链路绕开了 Docker 和 WSL 的资源开销对本地机器的负载小了很多工作时长明显延长了。最后分享一个小技巧给frankenphp.exe设置一个环境变量FRANKENPHP_WORKER_NUM可以控制启用多少个 worker 进程。默认情况下是 1 个如果你的机器是多核 CPU可以适当调大这个数值。但需要注意worker 进程之间不共享内存如果应用里用了基于文件或数据库的会话需要确认并发处理时没有冲突。我一般开发阶段设置成 1联调压测时调到核心数减一效果适中。这个项目还在快速迭代中有兴趣的可以从本地开发环境开始尝试用顺了再评估生产环境迁移。