CatchAdmin PHP 后台管理框架 Logo CatchAdmin

Laravel 使用 PHP TrueAsync 协程 I/O 性能实测

把 Laravel 请求放进协程里处理,性能到底能提升多少?30%?50%?200%?甚至 400%?

Laravel Octane 让 Laravel 适配有状态运行模式,使 Laravel 进程保持常驻,跳过重复的框架启动流程。然而,几乎没人真正尝试让 Laravel 变成异步框架。此前一种常见判断是:让 Laravel 异步化实在太难了。

TrueAsync 0.6.0 发布后,爱好者 YanGusik 在没有大幅改动代码的情况下,成功适配了框架的核心部分。这真的可能吗?听起来像某种技巧。很多人曾尝试让 Laravel 适配 Swoole 协程,最后诞生的是一个从零构建的新框架 Hypervel。Hypervel 采用等价 API 实现了一套新的框架代码。那么,这次又是怎么做到的?

性能基准测试

环境

  • OS:WSL2(Linux 5.15),16 核,7.8 GB RAM
  • DB:PostgreSQL 16(max_connections=500
  • 压测:k6,constant-arrival-rate,1000 req/s,持续 30 秒

这些测试使用未开启协程模式的 Swoole,因为 Laravel 尚未适配该模式。在纯合成协程测试中,Swoole 的表现略优于 FrankenPHP + TrueAsync。两者性能接近,在 12 个 worker 下都能达到约 10,000 req/s。

测试项目地址

工作负载

/bench 端点会对 PostgreSQL 顺序执行 10 条 SQL 查询:用户查询、文章列表、插入浏览记录、更新浏览计数、聚合查询、TOP-N 查询等。数据库包含 100 个用户、1,000 篇文章,以及持续增长的 post_views 表。

这是一种更贴近真实场景的工作负载,而非合成的 "Hello World" 测试。

吞吐量(req/s),目标 1,000 req/s

在 16 个 worker 下,TrueAsync 处理了 987 req/s,几乎达到了 1,000 req/s 的目标。Laravel Octane 的最佳结果是 601 req/s(Swoole ZTS),在相同 worker 数量下低了 64%。

测试使用 16 个 worker,是为了给阻塞式服务器提供更有利的测试条件。

FrankenPHP + TrueAsync 用 4 个 worker 就能像 16 个 worker 一样稳定处理每秒 1,000 个请求。

协程会在每次 PDO::query() 时让出控制权,使其他协程在当前协程等待数据库响应时继续执行。单个 worker 可以同时处理数十个请求:当一个协程等待 PostgreSQL 时,其他协程继续工作。

阻塞式服务器要达到同样的 990 req/s,需要更多 worker。

中位延迟(P50)

在 16 个 worker 下:TrueAsync 为 29 ms,Swoole Laravel Octane 为 1,640 ms,差距达到 56 倍。这些秒级延迟几乎都来自队列等待时间:

PHP 代码和 SQL 的执行速度相同。全部差异来自阻塞式服务器必须等待当前请求完成后,才能开始处理下一个请求。进程或线程只是在等待。TrueAsync 的 CPU 利用率更高,因为协程之间不会互相阻塞。它们在等待 I/O 时让出控制权。没有魔法,只有更高效的资源利用。

负载下的内存使用

用理论验证结果

基准测试结果可以用并发效率理论验证。

根据测试测量:T_cpu ≈ 5 ms(PHP 执行时间),T_io ≈ 23 ms(SQL 等待时间)。阻塞系数为:T_io / T_cpu = 23 / 5 = 4.6

Goetz 公式(Brian Goetz,《Java Concurrency in Practice》)用于确定最优并发任务数:

text
N_opt = N_cores × (1 + T_io / T_cpu)

对于拥有 16 个 worker 的阻塞式服务器(每个 worker 同时处理 1 个请求):

text
Throughput = 16 / (T_cpu + T_io) = 16 / 0.028 ≈ 571 req/s

Octane 实测结果:601 req/s,接近理论上限。

对于使用 4 个 worker 的 TrueAsync(最多约 200 个并发协程):

text
N_opt = 16 × (1 + 4.6) = 89.6 coroutines

200 个协程足以覆盖最优并发规模。根据 Little 定律(λ = L / W):

text
λ = 200 / 0.028 ≈ 7,142 req/s

这是理论最大值。实际结果约为 990 req/s,因为瓶颈在 PostgreSQL,而非 CPU。理论验证了这一点:在 I/O 密集型工作负载和 4.6 阻塞系数下,协程模型比阻塞模型更高效地利用 CPU。

基准测试结论

Swoole ZTS(线程)≈ Swoole NTS(进程)。线程模型的吞吐量提升为零。模型依旧是阻塞式:任意时刻每个 worker 处理一个请求。由于线程本地存储,ZTS 的内存消耗高出 5-10%。

Octane FrankenPHP ≈ Octane Swoole。两者都是阻塞式服务器,存在相同的根本限制。FrankenPHP 更节省内存(一个 Go 进程,而非多个独立 PHP 进程),但吞吐量表现基本相同。

最低收益为 30-40%。即使把阻塞式 worker 池扩展到 22-27 个以匹配吞吐量,TrueAsync 依然在延迟和内存消耗上占优。既然 TrueAsync 用 4 个 worker 就能完成,为什么还要消耗 22 个 worker?

核心结论是:对于 I/O 密集型工作负载(典型 Web 应用正是如此),TrueAsync 可以用少 5-6 倍的 worker 承载相同请求量,把延迟从数千毫秒降到几十毫秒,并将内存消耗减半。

更低的内存使用只是额外收益。在阻塞模型中,每个 worker 都是一个独立进程(或线程),拥有完整的 Laravel 副本:容器、配置、路由、中间件、数据库管理器。TrueAsync 中只有一个或少量 worker,内部协程共享同一套启动后的框架环境。被复制的主要是每个请求独有的部分(request、session、auth)。因此产生了差异:TrueAsync 为 308 MB,FrankenPHP Octane 为 400 MB。

本作品采用《CC 协议》,转载必须注明作者和本文链接