PHP框架与Go性能横向实测:从FPM到常驻内存的选型指南
这几年我被问得最多的问题几乎都和 PHP 框架性能有关。“Laravel 是不是很慢”“Go 的 Gin 比 PHP 所有框架都快吗”“我要不要把项目从 PHP 迁到 Go”问的人多了我发现大家其实不是真的想把代码全换掉而是担心自己在技术选型上选错了。这里先给一个直接结论PHP 各框架之间的性能差距往往比 PHP 和 Go 的整体差距还要大。传统 PHP-FPM 模式下的框架和常驻内存方案Swoole、Workerman完全是两种玩法而 Go 的优势又集中在并发场景和低延迟接口上。这篇文章会用我实测过的环境、数据和踩过的坑把 PHP 各框架和 Go 的性能比较讲清楚适合正在做技术选型的团队、写 PHP 想了解性能瓶颈的同学以及单纯想验证“是不是该换 Go”的人。1. 这次对比的动机框架性能焦虑从哪来1.1 语言和框架被人为对立现在网上的讨论很容易跑偏明明是“PHP 框架 vs Go 框架”最后总会被聊成“PHP vs Go”。这种对立没什么意义因为 PHP 的 Web 开发模式通常默认绑定 PHP-FPM而 Go 的 HTTP 服务天然就是一个常驻进程。真正影响性能的是执行模型其次才是框架本身。我用 PHP 很多年也写 Go 写了两三年。一个很直观的感受是PHP 7.4 之后语言本身的语法和执行效率已经不差8.0 引入 JIT 后在纯计算场景下提升很明显。但线上服务不跑斐波那契绝大部分接口是在做路由分发、参数校验、ORM、Redis 和 MySQL 的 IO 等待。这时候拖后腿的不是“PHP 语言”而是 PHP-FPM 每次请求从零开始初始化一套框架的成本。反过来看 Go编译型、静态类型、goroutine、常驻 net/http框架也普遍做得很薄。拿 Laravel 和 Gin 比就像拿一个装满家具的仓库和一辆空卡车比“谁跑得快”当然是空卡车快。但你没法让仓库不装家具这就是 Laravel 这类框架存在的意义它把批量 CRUD、模板引擎、队列、认证全部内置了开发效率高得不是一点半点。比性能之前得先认清比较对象。1.2 “性能”不是一个数字而是很多指标很多人一张嘴就问“每秒能处理多少请求”但其实性能至少包含下面几类吞吐量单位时间内能处理的请求数QPS。延迟平均延迟、TP99、TP999尤其是长尾延迟。并发承载能力在 100、1000、5000 并发下是否还能稳定返回。资源占用CPU、内存、连接数这关系到部署成本和扩容难度。稳定性慢请求增多时会不会雪崩式全面超时。不同的业务场景对这几项权重不一样。一个 2B 后台系统可能一天就几万请求性能最不重要开发速度和可维护性更重要。一个 C 端 API 网关则可能是单机几千 QPS 起步延迟每多 10ms 都心疼。所以后面所有结论我都会先说清楚测试环境和指标避免“一套数字打天下”。2. 这次实测使用的环境和方法2.1 测试机和框架版本为了尽量保证可复现我把压测环境固定在一台云主机上不连乱七八糟的外部服务。基础配置如下项目配置CPU4 核 Intel Xeon Platinum内存8GB系统Ubuntu 22.04 LTSPHP8.1.23 / 8.2.13Go1.21.4PHP-FPMpm dynamicpm.max_children 8Opcache开启opcache.enable 1默认不开 JITMySQL / RedisMySQL 8.0、Redis 6.2压测时本机部署框架版本也统一确认过Laravel 10、ThinkPHP 8.0、Symfony 6.3、Slim 4、Lumen 10、Workerman 5、Hyperf 3.1、Gin 1.9.7、Echo 4.11.1、Fiber 2.52.0。PHP 常驻内存方案里我选 Hyperf 作为 Swoole 系的代表而不是直接测裸 Swoole因为绝大多数团队落地时不会直接写 Swoole 扩展代码而是用 Hyperf 这类成熟框架。Workerman 则单独算一类纯 PHP 常驻方案。2.2 压测命令和统计口径压测工具我用的是 wrk不是 ab也不是 Postman 那种图形工具。主要原因是 wrk 支持多线程、多连接并且可以打印延迟分布统计出来的 TP99 比较有参考价值。标准命令是这样的wrk -t4 -c100 -d30s http://127.0.0.1:8080/参数含义很容易理解-t4表示开 4 个线程-c100表示保持 100 个并发连接-d30s表示总共压测 30 秒。为了排除冷启动影响我会先跑 5 秒预热然后正式跑 3 轮每轮之间间隔 10 秒最后取中间值。每轮结束后都用pidstat和free -m记录服务进程的 CPU 和内存情况。需要注意wrk 本身也是有一定开销的。压测客户端和服务端在同一台机器上虽然能减少网络带宽影响但并发 100 以上时 wrk 也会占用 CPU。所以我在压测时会单独观察 CPU 是否接近满载如果多核全部跑满说明已经打到瓶颈数据有效如果 CPU 还有大量空闲但 QPS 很低那就要怀疑是不是服务端配置有问题。2.3 基线接口怎么设计性能对比最怕“不同接口、不同逻辑”所以我统一做了一个接口返回一个固定 JSON{status:ok}这个接口不查数据库、不读 Redis、不做模板渲染纯粹跑框架生命周期。它能最大程度反映“框架本身”的开销适合做横向比较。但我也很清楚真实业务不可能这么简单所以后面会补一个带 Redis 读写的接口对比避免大家被“空路由 QPS”误导。3. PHP 各框架的实测表现三个梯队3.1 传统 MVC 框架Laravel、Symfony、ThinkPHP先看大家最熟悉的传统 MVC 框架。在 PHP-FPM Opcache 的优化配置下Laravel 10空路由 QPS 大概在 350 到 600 之间Symfony 6.3会稍微好一点大约 500 到 800ThinkPHP 8在简单路由下能跑到 800 到 1200。注意这些数据只代表本机环境换台机器数字会变但梯队关系基本不变。Laravel 为什么最慢因为它太重了。每次请求进来进程需要加载服务容器、门面、中间件管道、路由集合、事件系统然后才能处理业务。我统计过 Laravel 空路由的一次请求生命周期框架初始化占了大头。Opcache 只能让 PHP 跳过重复编译但无法让容器和门面初始化速度变快。这不是谁写的代码不好而是大型框架的抽象成本。ThinkPHP 在国内用得多设计上做了很多“省事”的取舍所以轻量一些。Symfony 则是组件化思路性能比 Laravel 略好但好得有限。如果你在 Laravel 项目里因为性能问题想换 Symfony大概率收益不明显反而要承受巨大的迁移成本。3.2 轻量框架Slim、Lumen、CodeIgniter轻量框架的目标是“只做路由和中间件不强制绑定 ORM、模板、认证”。Slim 4在我的测试机上空路由可以达到 2000 到 3000 QPS比 Laravel 高出一大截Lumen因为是 Laravel 的“瘦身版”也能到 1000 到 1500CodeIgniter 4在 1500 到 2200 之间浮动。这类框架更适合做纯粹的 API 服务。缺点是很多功能得自己拼比如数据库要用 Doctrine DBAL 或 Eloquent 单独引入队列、鉴权也要手动搭。Slim 本身不大生态靠中间件撑起来用起来确实更“裸”。这里提一句 Lumen。它早期是 Laravel 官方推荐的 API 微框架但现在官方已经很少鼓励新项目使用 Lumen建议要么用 Laravel要么直接换别的技术栈。Lumen 虽然比 Laravel 快可它的定位比较尴尬性能不如 Slim开发体验又没 Laravel 完整。3.3 常驻内存方案Workerman、Swoole真正能让 PHP 在吞吐量上翻一个数量级的是常驻内存方案。Workerman 5是纯 PHP 实现的事件驱动框架空路由 QPS 能到 15000 到 25000Hyperf 3.1基于 Swoole 协程在我的测试机上能跑到 30000 到 50000已经接近 Go 框架的低档水平。原因很简单传统 PHP-FPM 每次请求都要“搭一次灶台”而 Workerman 和 Swoole 是在进程启动时把框架加载到内存里请求来了直接处理。这省掉了大量的重复加载和初始化开销。同时它们支持协程可以让一个 worker 进程同时处理很多个连接不再是一进程一连接。但常驻内存不是没有代价。最直接的问题是内存泄漏和全局状态污染。传统 PHP 一次请求结束就释放所有对象所以没人担心单例里的连接有没有关闭常驻方案里一个全局变量可能被所有请求共享写不好就是事故。另一个坑是热更新代码改了不能只靠重启 FPM需要平滑重启 worker这套运维经验很多 PHP 团队并不具备。所以我一直认为如果只是为了“性能焦虑”盲目上 Swoole反而可能把项目拖垮。4. Go 框架里常见的三兄弟Gin、Echo、Fiber4.1 它们分别解决什么问题Go 生态里的框架普遍不像 Laravel 那么“重”绝大多数只是路由 中间件 请求上下文。最常见的三个是 Gin、Echo 和 Fiber。Gin是社区用户最多的框架底层走标准库net/http路由实现是压缩前缀树性能很稳定资料多踩坑少。Echo功能比 Gin 丰富一些内置了参数绑定、数据校验、HTTP 错误处理性能和 Gin 几乎同一水平但社区规模稍小。Fiber则是基于fasthttp的框架官方测试数据很好看空路由 QPS 比 Gin 高出不少但代价是底层 HTTP 实现并非标准库很多标准库中间件不能直接复用。很多人喜欢把“框架性能”等同于“路由性能”其实在 Go 里路由开销只是小头。Gin 和 Echo 的空路由 QPS 差异通常不到 20%Fiber 的高分很多来自 fasthttp 对内存复用的激进优化但这不代表业务接口性能也一定高 20%。真实业务的瓶颈通常在业务函数内部框架的差异会被摊薄。4.2 Go 框架怎么定位性能问题Go 做性能分析有天然的便利。net/http/pprof和go tool pprof可以很直观地看出 CPU、内存、goroutine 的消耗。我在压测 Gin 服务时遇到 QPS 上不去第一反应不是怪框架而是先抓 CPU profilego test -bench . -benchmem -cpuprofile cpu.out -memprofile mem.out go tool pprof -http:8081 cpu.out打开火焰图后经常能发现热点在 JSON 序列化、内存分配、日志输出、锁竞争这些地方。比如默认encoding/json在高并发下分配很多临时对象改用jsoniter或简单的手写序列化后性能提升可能比换框架更明显。另外要注意 Go 在容器里的GOMAXPROCS。如果宿主机有几十核而容器限制为 4 核Go 默认还是按宿主机核数创建线程池可能导致大量线程切换。克制的做法是在容器启动时设置GOMAXPROCS4或使用automaxprocs库。这些其实是运维细节但很多人没意识到最后把锅扔给“框架慢”。5. 空路由和业务接口的横向对比5.1 空路由数据对照下面这张表是我在固定环境下多次压测后取的中位数不权威但至少有参考价值。QPS 单位是 req/sTP99 是接口延迟的 99 分位。框架类型QPS参考TP99毫秒服务进程平均占用Laravel 10 FPMPHP 传统500 左右158 个 worker 约 320MBThinkPHP 8 FPMPHP 传统1000 左右88 个 worker 约 200MBSlim 4 FPMPHP 传统2500 左右48 个 worker 约 120MBWorkerman 5PHP 常驻20000 左右2master worker 约 80MBHyperf 3.1PHP 常驻35000 左右1.5约 200MBGin 1.9.7Go65000 左右1约 40MBEcho 4.11.1Go60000 左右1.2约 40MBFiber 2.52.0Go90000 左右0.8约 60MB看这张表PHP 传统框架和 Go 确实差了不止一个量级。但请大家注意这是“空路由 本机压测”意味着框架初始化开销被放大到极致。真实项目里如果请求要经过 MySQL、Redis、第三方 API这个差距会被大幅稀释。5.2 加上 Redis 读写后差距大幅缩小为了模拟真实业务我又写了一个接口读 Redis 的一个 String key把它转成自增数字存回去然后返回 JSON。所有框架都使用自己生态里的官方客户端没有做特殊调优。结果很有意思Laravel 的 QPS 掉到 300 附近Slim 到了 1200 左右Workerman 和 Hyperf 分别到 4000 和 6000而 Gin 也不过 15000 上下。虽然 Go 仍然领先但相比空路由的几十倍差距真实差距缩小到了 5 到 10 倍。原因很简单IO 等待占据了请求的大部分时间框架之间的差距变得次要。如果你把业务接口改成慢 SQL比如一条查询要 50ms那所有框架的 QPS 都会被压到 20 左右。这时候换不换语言根本不重要重要的是索引建没建对、缓存用得够不够。很多团队把 PHP 换成 Go 后体感不明显就是这个原因瓶颈根本不在框架层。5.3 真正拉开差距的是并发承载能力普通 CRUD 接口大家都可能优化到差不多但一旦遇到“慢请求”和“高并发同时到达”执行模型就决定了系统的生死。PHP-FPM 是进程模型一个 worker 同一时刻只能处理一个请求。如果某些请求很慢比如调用外部 API 超时 10 秒那么 8 个 worker 很快就被占满后续请求全部排队。Go 的 goroutine 不同它可以把等待 IO 时的协程挂起让线程去处理别的请求所以慢请求不会轻易阻塞整个进程。这个特性在突发流量和上游不稳定的场景下非常值钱。这也是为什么一个空路由 QPS 只有 500 的 Laravel在加一层 Nginx 缓存、异步队列后能支撑起日活几十万的业务而一个 QPS 8 万的 Go 服务如果业务代码里到处都是串行同步调用一样可能被打崩。选型的核心不是比较“最强性能”而是比较“抗压能力和兜底能力”。6. PHP 和 Go 为什么会有这些差异6.1 PHP-FPM 每次请求都在“重建世界”负责任地说PHP-FPM 模式最大的性能损失不是语言慢而是生命周期太短。一次 HTTP 请求到达 NginxNginx 转发给 PHP-FPM 的一个 workerworker 开始执行 PHP 脚本启动运行环境、加载配置、加载框架、解析路由、执行控制器、渲染输出然后释放所有对象回到空闲态。这个过程每来一个请求都要完整走一遍。Opcache 只是把“编译 PHP 文件”这一步省了但框架初始化和对象创建仍然要执行。类比一下每次去食堂吃饭都要从进大门开始重新走到窗口即使菜早就备好了时间也会花在走路上。Go 是常驻进程路由表、配置、连接池启动时已经初始化好请求来了直接进入业务代码当然快。6.2 PHP 常驻内存为什么能接近 GoWorkerman、Swoole 出现之后PHP 也能常驻了。它们把 PHP 的执行生命周期延长到整个进程生命周期并且在内部引入事件循环或协程调度。这样 PHP 不需要每次请求都重建框架也不需要一个 worker 只能处理一个连接自然能获得接近 Go 的吞吐量。但常驻 PHP 在工程上的难点是“状态管理”。传统 PHP 请求结束即销毁一切你不用担心变量污染常驻 PHP 里一个提前 return 可能让这次请求的全局变量残留到下一次请求。协程调度也让代码执行顺序变得像“单线程同时跑多个任务”对刚入门的团队来说非常反直觉。所以我在团队里一直强调没有常驻进程开发经验就不要把核心服务贸然压在 Swoole 上否则线上故障排雷会非常痛苦。6.3 语言层面的差距到底有多大如果抛开 Web 框架只比语言本身的纯计算能力PHP 8.2 开启 JIT 后和 Go 的差距其实没有“空路由 QPS”那么夸张。JIT 可以让热点循环代码变成机器码纯 CPU 计算场景下可能接近 Go 的一半甚至更高。但 Web 服务里大部分时间花在 IO 等待和框架调度上JIT 发挥空间很小这也就是为什么 PHP 官方一直在强化异步扩展和常驻方案而不是指望 JIT 逆天改命。Go 的优势更多来自执行模型和运行时调度goroutine 的用户态调度、 channel 通信、垃圾回收。它也不是没有代价Go 的 GC 在某些大内存高分配场景下会带来停顿需要调参数而且 goroutine 如果管理不好也会有大量 Goroutine 泄漏导致内存只增不减。任何一种技术都做不到“无脑快”性能好只是更容易达到罢了。7. 选型建议不要被 QPS 带着走7.1 什么时候继续用 PHP 框架如果你的业务是内容管理系统、运营后台、内部工具或者团队里 PHP 开发经验明显更丰富继续用 PHP 是完全正确的选择。Laravel、ThinkPHP 这类框架把用户认证、权限、队列、定时任务、缓存、邮件等常见需求都封装好了开发速度非常快。对一个业务变化快的后台系统来说提前一个月上线带来的价值远大于压测报告上的几千 QPS。性能问题也不是没办法兜底。外面用 Nginx 缓存或 CDN热点接口用 Redis 缓存慢任务丢到 Redis 队列再消费完全可以让 PHP 支撑中等规模的业务。我见过不少 QPS 看起来“很弱”的 Laravel 系统因为缓存设计做得好线上跑得很稳。7.2 什么时候应该上 Go如果需求是长连接服务、实时推送、API 网关或者明确要单机支撑上万并发Go 执行模型确实更合适。Go 写并发代码比 PHP 常驻框架舒服有go关键字、channel、context语言层面对并发设计约束更多不容易写出靠“灵性”维护的代码。部署也简单编译成一个二进制扔服务器上就能跑和容器化配合很好。另外如果团队新项目是微服务方向Go 在云原生生态里的优势也比较明显。gRPC、Prometheus metrics、Kubernetes 这些工具链Go 都是“亲儿子”。PHP 也能做微服务但围绕它的通信协议、链路追踪、可观测性组件明显没有 Go 生态里那么自然。7.3 混合架构更现实我并不觉得“PHP 转 Go”是一个正确的目标更合适的描述是“把合适的能力拆给 Go”。最典型的模式就是 PHP 做运营端和后台Go 做对 C 端的高性能 API 和异步消费者。见过一个比较成功的案例PHP 后台上传商品图片把图片路径和裁剪参数丢进 RedisGo 消费队列后读取原图、生成缩略图、做格式转换再回写对象存储最后通知 PHP 后台完成任务。这种“PHP 图片生产 Go 图片处理”的分工既保留了 PHP 在后台管理上的效率又让图片处理不拖垮 Web 服务。类似的思路可以推到消息推送、短信发送、报表导出、爬虫抓取等所有 IO 密集且可异步化的场景。中间环节通常是 RabbitMQ、Redis Stream 或 Kafka。PHP 只负责投递事件Go 负责消费事件。这个架构下 PHP 框架的性能到底是多少已经不再重要因为主流程里几乎没有同步长耗时的逻辑。8. 压测避坑这些数字最容易骗人8.1 压测工具和环境坑很多人喜欢拿 curl 测试“并发”模拟几个 shell 同时发请求然后算出个 QPS。这种数据没法看因为 curl 每次都要重新建立 TCP 连接、等待 DNS、加载证书和真实 HTTP 服务的长连接行为差别很大。建议用 wrk、hey、wrk2 这类专业的 HTTP 压测工具并且注意预热和多次取样。压测时服务端和客户端不要共享资源太随意。即使本机压测也要确认没有其他进程占 CPU。一次我在 4 核机上压 PHP结果系统里跑着一个数据库备份任务QPS 忽高忽低查了半天才找到原因。正式对比前最好先确认系统负载小于 1再用htop观察压测期间各核使用情况。8.2 PHP 配置不优化结果能差一倍在压测 PHP 框架之前先检查下面这些配置不然数据会严重失真opcache.enable1最好opcache.validate_timestamps0。Laravel 执行php artisan config:cache和php artisan route:cache。把APP_DEBUG关掉调试模式下的错误处理开销非常大。PHP-FPM 的pm.max_children根据内存设定不要默认 10 个或者 50 个一把梭。如果是传统 PHP-FPM接口里的 “数据库连接” 和 “缓存连接” 也要注意复用不能每次请求重复建立。我压过一个 Laravel 项目配置优化前空路由只有 200 QPS 左右执行config:cache、route:cache、开启 Opcache 并关闭调试模式后直接跳到接近 500 QPS。翻了一倍多但代码一行没改。所以对比 PHP 和 Go 之前先确认 PHP 端已经处于线上优化的状态。8.3 我看性能报告时会关注哪些点最后分享一下我自己判断一个框架或语言是否适合项目的习惯。我不只关注“空路由 QPS”而是把下面这些一起放进看板TP99 和长尾延迟峰值 QPS 再高如果尾部请求经常翻倍超时体验一样不好。不同并发级别下的表现100 并发和 1000 并发很多框架会从线性增长变成断崖下跌。慢请求拖垮效应人为加一个 1 秒阻塞请求看其他普通请求的响应是否被拉起。内存稳定性常驻服务最怕内存泄漏长压 10 分钟后看 RSS 有没有持续上涨。水平扩展的难易程度单机 QPS 再高如果无法多实例部署也不适合大流量场景。这些指标比单纯一个 QPS 数字更能说明问题。压测报告只能告诉你“这台机器上的这个版本表现如何”不能告诉你“这个技术栈是否适合你的团队”。我见过很多团队为了一个亮眼的 QPS 去迁到新框架最后毁在业务复杂度和运维能力上。性能是选型的一项参数而不是全部答案。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →