欢迎访问晨星博客!
一个聚合接口要连着调三个外部服务、再查五次数据库,在 PHP-FPM 的阻塞模型里只能一段一段等。Accesto 的基准测试把这种日常场景量化了出来:串行执行 PDO 查询和 Guzzle 同步请求,整轮耗时 2390ms。用户感知到的“接口有点慢”,很多时候不是业务逻辑重,而是等待被叠加了。
PHP 里能解决这类问题的方案不止一个,但 Guzzle、amphp、Swoole 常常被放进同一张对比表里讨论,这本身就是个误区——它们解决的层次并不相同。选错的代价不是慢一点,而是引入了一整套团队不熟悉的运行模型。

Guzzle 首先是一个 HTTP 客户端。它的异步能力只覆盖网络请求这一层,一旦落到 PDO 上就是同步的,没有绕过去的办法。这意味着它能压缩外部 API 的等待时间,却动不了数据库查询那一段。
amphp 是建立在 PHP 8.1+ Fibers 之上的非阻塞协程方案,通过 Composer 安装即可使用,事件循环由 Revolt 提供,讲究结构化并发。它能同时并发数据库和 HTTP 两件事,代价是要用异步驱动的写法。
Swoole 是 C++ 扩展,协程、事件循环、HTTP/TCP 服务器一体内建。性能上限最高,功能最全,但需要编译安装,协程 API 需要专门学习,框架往往要做适配。
| 方案 | 本质 | 最擅长 | 接入成本 | 生态兼容 |
|---|---|---|---|---|
| Guzzle | HTTP 客户端(异步仅限 HTTP) | 并发调用外部接口 | 低,Composer 即可 | 极好,Laravel、WordPress 插件通用 |
| amphp | Fibers + Revolt 用户态协程 | 脚本与任务级并发 I/O | 中,需改写异步风格 | 受限,需异步生态组件 |
| Swoole | C++ 扩展,协程 + 常驻服务 | 高并发常驻服务、WebSocket | 高,需装扩展、写协程代码 | 需适配器 |
Accesto 的第一组测试里,Guzzle 同步串行耗时 1272ms,换成异步请求后降到 260ms。这个提升幅度对任何以调用外部 API 为主的接口都足够可观,而且改动量往往只是把循环里的请求改成并发发起。
加入五次数据库查询后,差距被拉开:
| 执行方式 | 耗时 | 加速比 |
|---|---|---|
| 串行(PDO + Guzzle 同步) | 2390ms | 1.0x |
| Guzzle 异步(仅 HTTP 并发,数据库串行) | 1318ms | 1.8x |
| amphp 全并发(数据库 + HTTP) | 283ms | 8.4x |
| Swoole 全并发(数据库 + HTTP) | 260ms | 9.2x |
Guzzle 把 2.4 秒压到 1.3 秒,然后停在那里——剩下的时间仍然花在五段串行的数据库查询上。amphp 和 Swoole 之所以都能到 260ms 附近,是因为查询和请求被放进同一个调度器并行执行。完整数据和方法可以在 Accesto 的原始基准里核对。
Swoole 比 amphp 快的那 8% 左右,来自 C 层协程挂起与用户态 Fiber 的开销差异。真实查询有实际计算量时,这个差距会进一步缩小——所以不要拿这 8% 当作选型理由。
如果接口慢的根因是“要等三个外部服务返回”,Guzzle 是最划算的选择。无需扩展、无需常驻进程、无需改变部署方式,PHP-FPM 里照样跑。WordPress 站点里调用第三方接口的定时任务、需要批量拉取远程数据的插件,都在这条路线上。
它的边界也很清楚:并发只发生在 HTTP 层。只要业务里还有串行数据库查询、串行文件读写、串行缓存访问,这些等待就纹丝不动。
如果代码已经在 Swoole 环境里,又不想重写基于 Guzzle 的第三方 SDK,可以借助 Guzzle-Swoole 这类适配层,让 Guzzle 在协程环境下运行,支持 Guzzle v6/v7,Elasticsearch、AWS 这类 SDK 也能复用。这是在不动业务代码的前提下拿到协程化收益的办法。
amphp 和 Swoole 的收益主要来自把数据库也拉进并发,代价同样在这里。
Swoole 的协程代码长这样,用的是资料中给出的典型结构:
SwooleRuntime::enableCoroutine(SWOOLE_HOOK_ALL);
$wg = new SwooleCoroutineWaitGroup();
foreach ($endpoints as $region => $sql) {
$wg->add();
go(function () use ($region, $sql, $wg, &$results) {
try {
$results[$region] = runQuery($sql);
} finally {
$wg->done(); // 异常也要归还计数
}
});
}
$wg->wait();
常驻进程里有个容易被忽略的前提:连接必须复用,否则每个请求都新建连接,反而比 FPM 更慢。所以连接池不是优化项,是必需品——用 SwooleCoroutineChannel 做一个固定大小的池,取用与归还都走协程,是常见写法。此外,优雅退出时关闭连接池、刷日志、保存状态,也都需要显式处理。
还有一个硬性限制:依赖阻塞式 C 扩展的逻辑无法协程化。资料里提到的那类扩展一旦进入协程上下文,整个进程还是会被卡住,进度收益瞬间归零。选型前先盘点扩展依赖,比看基准数字重要。
amphp 走的是另一条路:不装扩展,纯 Composer 生态,用 Fibers 表达并发,amphp/http-client 就是基于 Fibers 的异步客户端。它更适合脚本、队列消费者、需要复杂异步编排的后台任务;代价是数据库层要换成异步驱动,生态组件比 Swoole 更窄。
如果目标是把接口 QPS 推上去,讨论对象就不是“客户端怎么并发”,而是“服务怎么常驻”。一份针对 hello world JSON 端点的压测(k6,100 恒定虚拟用户,60 秒,4 vCPU 8GB)给出了量级参考:
| 运行方式 | 请求/秒 | p95 延迟 | 内存占用 |
|---|---|---|---|
| PHP-FPM 8.3 + Nginx | 4,200 | 35ms | 280MB |
| FrankenPHP worker 模式 | 14,500 | 12ms | 180MB |
| RoadRunner | 13,800 | 14ms | 210MB |
| Swoole | 38,000 | 4ms | 120MB |
| ReactPHP | 11,000 | 18ms | 140MB |
需要提醒的是,这是在最轻的端点上测的。换成带路由、依赖注入、ORM 查询和 JSON 渲染的完整 Symfony 端点后,启动开销不再是瓶颈,各方案之间的差距会明显收窄。把 hello world 的数字当作业务接口的预期提升,是选型里最常见的误判。
另一个真实案例说明了 FPM 的天花板在哪:一个电商促销接口,DAU 50 万,大促峰值 QPS 2000,P95 延迟 300ms,其中约 60% 是外部支付和物流 API 的等待。按 80 个 worker、每请求 0.3 秒计算,理论上限约 266 QPS,峰值时 502 几乎是必然结果。横向扩容能解决,但成本线性上涨;而瓶颈本质上是 I/O 等待,协程模型正好对应这类问题。
把结论压缩成判断路径,比记参数更有用。
只有当接口里存在可并行的外部等待——多个下游 API、多段独立查询、WebSocket 长连接、BFF 聚合——异步才有意义。流量不高、部署简单的项目,FPM 加 OPcache 足够,把重活丢给消息队列反而更好维护。团队不熟悉协程或事件循环模型时,引入 amphp 或 Swoole 带来的长期维护成本,很可能超过那几百毫秒。
对应到具体选择:
| 你的情况 | 建议 |
|---|---|
| 只是要并发调用几个外部 API,仍跑在 FPM 上 | Guzzle 异步,改动最小 |
| 已在 Swoole 环境,有大量基于 Guzzle 的 SDK | 用 Guzzle-Swoole 适配,避免重写 |
| 脚本、后台任务,需要数据库和 HTTP 同时并发,不想装扩展 | amphp |
| 高并发常驻服务、WebSocket、需要压榨单机吞吐 | Swoole |
| 想升级部署方式但不想碰协程 | FrankenPHP worker 模式或 RoadRunner |
还有一类信号值得警惕:依赖阻塞式 C 扩展、连接池没有设计、没有优雅退出机制。这三项只要缺一项,上 Swoole 就是在给自己埋故障。反过来,只要业务确实卡在 I/O 等待上,并且团队愿意承担运行模型的变化,Swoole 的吞吐和延迟优势是实打实的。
异步不是性能开关,是运行模型的切换。先量一下你的接口时间到底花在哪里,再决定要不要为那部分等待付出相应的复杂度。
声明:原创文章请勿转载,如需转载请注明出处!