这几年我被问得最多的问题,几乎都和 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 测试机和框架版本
为了尽量保证可复现,我把压测环境固定在一台云主机上,不连乱七八糟的外部服务。基础配置如下:
| 项目 | 配置 |
|---|---|
| CPU | 4 核 Intel Xeon Platinum |
| 内存 | 8GB |
| 系统 | Ubuntu 22.04 LTS |
| PHP | 8.1.23 / 8.2.13 |
| Go | 1.21.4 |
| PHP-FPM | pm = dynamic,pm.max_children = 8 |
| Opcache | 开启,opcache.enable = 1,默认不开 JIT |
| MySQL / Redis | MySQL 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 到 800;ThinkPHP 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 到 1500;CodeIgniter 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 到 25000;Hyperf 3.1基于 Swoole 协程,在我的测试机上能跑到 30000 到 50000,已经接近 Go 框架的低档水平。
原因很简单:传统 PHP-FPM 每次请求都要“搭一次灶台”,而 Workerman 和 Swoole 是在进程启动时把框架加载到内存里,请求来了直接处理。这省掉了大量的重复加载和初始化开销。同时它们支持协程,可以让一个 worker 进程同时处理很多个连接,不再是一进程一连接。
但常驻内存不是没有代价。最直接的问题是内存泄漏和全局状态污染。传统 PHP 一次请求结束就释放所有对象,所以没人担心单例里的连接有没有关闭;常驻方案里一个全局变量可能被所有请求共享,写不好就是事故。另一个坑是热更新:代码改了不能只靠重启 FPM,需要平滑重启 worker,这套运维经验很多 PHP 团队并不具备。所以我一直认为,如果只是为了“性能焦虑”盲目上 Swoole,反而可能把项目拖垮。
4. Go 框架里常见的三兄弟:Gin、Echo、Fiber
4.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 profile:
go test -bench . -benchmem -cpuprofile cpu.out -memprofile mem.out go tool pprof -http=:8081 cpu.out打开火焰图后,经常能发现热点在 JSON 序列化、内存分配、日志输出、锁竞争这些地方。比如默认encoding/json在高并发下分配很多临时对象,改用jsoniter或简单的手写序列化后性能提升可能比换框架更明显。
另外要注意 Go 在容器里的GOMAXPROCS。如果宿主机有几十核,而容器限制为 4 核,Go 默认还是按宿主机核数创建线程池,可能导致大量线程切换。克制的做法是在容器启动时设置GOMAXPROCS=4或使用automaxprocs库。这些其实是运维细节,但很多人没意识到,最后把锅扔给“框架慢”。
5. 空路由和业务接口的横向对比
5.1 空路由数据对照
下面这张表是我在固定环境下多次压测后取的中位数,不权威但至少有参考价值。QPS 单位是 req/s,TP99 是接口延迟的 99 分位。
| 框架 | 类型 | QPS(参考) | TP99(毫秒) | 服务进程平均占用 |
|---|---|---|---|---|
| Laravel 10 + FPM | PHP 传统 | 500 左右 | 15 | 8 个 worker 约 320MB |
| ThinkPHP 8 + FPM | PHP 传统 | 1000 左右 | 8 | 8 个 worker 约 200MB |
| Slim 4 + FPM | PHP 传统 | 2500 左右 | 4 | 8 个 worker 约 120MB |
| Workerman 5 | PHP 常驻 | 20000 左右 | 2 | master + worker 约 80MB |
| Hyperf 3.1 | PHP 常驻 | 35000 左右 | 1.5 | 约 200MB |
| Gin 1.9.7 | Go | 65000 左右 | 1 | 约 40MB |
| Echo 4.11.1 | Go | 60000 左右 | 1.2 | 约 40MB |
| Fiber 2.52.0 | Go | 90000 左右 | 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 请求到达 Nginx,Nginx 转发给 PHP-FPM 的一个 worker,worker 开始执行 PHP 脚本:启动运行环境、加载配置、加载框架、解析路由、执行控制器、渲染输出,然后释放所有对象,回到空闲态。
这个过程每来一个请求都要完整走一遍。Opcache 只是把“编译 PHP 文件”这一步省了,但框架初始化和对象创建仍然要执行。类比一下:每次去食堂吃饭都要从进大门开始重新走到窗口,即使菜早就备好了,时间也会花在走路上。Go 是常驻进程,路由表、配置、连接池启动时已经初始化好,请求来了直接进入业务代码,当然快。
6.2 PHP 常驻内存为什么能接近 Go
Workerman、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 后台上传商品图片,把图片路径和裁剪参数丢进 Redis,Go 消费队列后读取原图、生成缩略图、做格式转换,再回写对象存储,最后通知 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.enable=1,最好opcache.validate_timestamps=0。- 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 去迁到新框架,最后毁在业务复杂度和运维能力上。性能是选型的一项参数,而不是全部答案。