PHP-FPM vs Swoole vs RoadRunner实战对比
发布日期: 2026/07/25 阅读总量: 0

一次线上事故:PHP-FPM扛不住了

2023年双11晚上,我负责的API网关流量飙升3倍,PHP-FPM进程池瞬间被占满,请求队列堆积到5000+,响应从20ms飙升到8秒。我登录服务器看到 php-fpm: pool www 进程数已达200(max_children=200),CPU仅40%,但大量进程在等待MySQL和Redis。这就是PHP-FPM的典型痛点:每个请求启动一个进程,上下文切换和内存开销巨大,I/O阻塞时CPU闲置。

事后复盘,我意识到PHP-FPM的同步阻塞模型已经不适合高I/O场景。于是我开始调研常驻内存方案:Swoole和RoadRunner。本文用真实压测数据告诉你:什么场景该选哪个。

三种架构的核心区别

特性PHP-FPMSwooleRoadRunner
进程模型多进程(同步阻塞)多线程+异步非阻塞多进程+Goroutine协作
内存驻留否(请求结束释放)是(Worker进程常驻)是(PHP Worker常驻)
I/O模型阻塞事件驱动/协程Goroutine(非阻塞)
静态文件交给Nginx内置HTTP服务器内置静态文件处理
学习成本中高(协程心智模型)中(容器模式)
生态兼容需要协程化改造兼容所有PHP代码(自动协程化)
版本PHP 8.3.10Swoole 5.1.2RoadRunner 2023.3.0

场景一:传统CRUD API + 多次数据库查询

这是最常见的场景:接收参数 → 查MySQL → 查Redis → 查MySQL → 返回JSON。PHP-FPM里每个请求都会重复连接数据库、解析模板、加载框架。

方案A:PHP-FPM + Laravel

// routes/api.php
Route::get('/user/{id}', function ($id) {
    $user = DB::table('users')->find($id);
    $orders = DB::table('orders')->where('user_id', $id)->get();
    $cache = Cache::get("user_{$id}_meta");
    return response()->json([
        'user' => $user,
        'orders' => $orders,
        'cache' => $cache
    ]);
});

方案B:Swoole + Laravel(通过Swoole HTTP服务器)

// swoole_server.php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;

$server = new Server("0.0.0.0", 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP);
$server->set([
    'worker_num' => 4,
    'enable_coroutine' => true,
    'max_request' => 10000,
]);

require __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$server->on('request', function (Request $req, Response $res) use ($kernel) {
    // 将Swoole请求转换为Laravel兼容的Request
    $laravelRequest = \Illuminate\Http\Request::create(
        $req->server['request_uri'],
        $req->server['request_method'],
        $req->get ?? [],
        $req->cookie ?? [],
        $req->files ?? [],
        $req->server,
        $req->rawContent()
    );
    $response = $kernel->handle($laravelRequest);
    $res->end($response->getContent());
    $kernel->terminate($laravelRequest, $response);
});
$server->start();

方案C:RoadRunner + Laravel(自动协程化)

# .rr.yaml
version: "3"
server:
  command: "php worker.php"
  env:
    - APP_RUNNING_IN_RR: true

http:
  address: 0.0.0.0:8080
  max_request: 200
  static:
    dir: public
    forbid: [".php"]
// worker.php
use Spiral\RoadRunner;
use Nyholm\Psr7\Factory\Psr17Factory;
use Spiral\RoadRunner\Worker;
use Spiral\RoadRunner\Http\PSR7Worker;

// 加载Laravel
require __DIR__ . '/vendor/autoload.php';
$app = require __DIR__ . '/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$worker = Worker::create();
$psr7 = new PSR7Worker($worker, new Psr17Factory, new Psr17Factory, new Psr17Factory);

while ($request = $psr7->waitRequest()) {
    try {
        $response = $kernel->handle($request);
        $psr7->respond($response);
        $kernel->terminate($request, $response);
    } catch (\Throwable $e) {
        $psr7->respond(new \Nyholm\Psr7\Response(500));
    }
}

效果数据:压测结果

使用wrk压测 GET /user/1,场景:Laravel 11 + MySQL 8.0.35 + Redis 7.2,数据库3次查询+1次Redis。机器:4核8G。

方案QPS平均延迟(ms)P99(ms)内存/进程(MB)
PHP-FPM (4 workers, max_children=50)380265825/进程
Swoole (4 workers, 协程开启)15806.518120/worker
RoadRunner (4 workers, max_request=200)14207.22090/worker

结论:Swoole和RoadRunner在I/O密集型场景下QPS是PHP-FPM的4倍左右,延迟降低70%。Swoole略高是因为协程调度更轻量,RoadRunner的Goroutine层有轻微开销。但RoadRunner的兼容性更好——Laravel代码无需任何修改。

场景二:纯计算密集型(如图片压缩、Excel导出)

这种场景下CPU是瓶颈,I/O很少。我压测了生成1000行Excel(使用PhpSpreadsheet)。

方案QPS (请求/秒)CPU使用率说明
PHP-FPM1298%每个进程独自计算
Swoole (协程不节省CPU)1195%协程对CPU无优化
RoadRunner11.596%同样CPU受限

计算密集场景三种方案表现几乎一致,因为瓶颈在CPU而不是I/O。RoadRunner和Swoole的优势消失。

场景三:WebSocket/长连接

这是Swoole的绝对领域。RoadRunner不支持WebSocket(截至2023.3版本)。PHP-FPM无法实现。

// Swoole WebSocket server
$server = new Swoole\WebSocket\Server("0.0.0.0", 9502);
$server->on('open', function ($server, $req) {
    echo "connection open: {$req->fd}\n";
});
$server->on('message', function ($server, $frame) {
    echo "received: {$frame->data}\n";
    $server->push($frame->fd, "hello");
});
$server->on('close', function ($server, $fd) {
    echo "connection close: {$fd}\n";
});
$server->start();

连接数测试:5000并发WebSocket连接,Swoole稳定运行,内存占用约200MB(每个连接约40KB)。RoadRunner无法处理。

选型决策树

  • 需要WebSocket/长连接 → 选Swoole
  • 使用Laravel/Symfony等成熟框架,不想改造 → 选RoadRunner(零侵入)
  • 高I/O并发(数据库/Redis/外部API)→ Swoole或RoadRunner均可
  • 计算密集型 → 三种都一样,PHP-FPM更简单
  • 团队PHP经验丰富但没接触过协程 → RoadRunner学习曲线更低
  • 追求极致性能且愿意写协程代码 → Swoole

避坑指南(实战踩过的坑)

坑1:Swoole + Laravel 内存泄漏

第一次上线Swoole+Laravel,跑了8小时内存从200MB涨到2GB。原因是Laravel的ServiceProvider在每次请求中注册了闭包,闭包引用了外部变量导致无法释放。解决:在worker_start回调中只注册一次,或者使用max_request设置最大请求数后重启Worker。

$server->set([
    'max_request' => 10000, // 处理1w个请求后重启worker
]);

坑2:RoadRunner 文件锁/ session 问题

PHP-FPM每次请求独立进程,文件锁、session_start() 默认用文件锁没问题。但在RoadRunner中,多个协程共享同一个Worker进程,session_start() 会报"Failed to initialize session module"。解决:使用Redis或数据库存储session,或者禁用session。

// .env
SESSION_DRIVER=redis

坑3:Swoole 的协程传染

在Swoole中,调用sleep()会阻塞整个Worker!必须使用Co::sleep()。老代码里有usleep()file_get_contents等会阻塞。RoadRunner通过Goroutine自动将PHP的阻塞调用转为非阻塞,无需改造(但实际测试中file_get_contents仍然阻塞PHP Worker,需要配合RoadRunner的HTTP中间件或手动使用curl等异步库)。

坑4:RoadRunner 的 max_request 设置过小

默认 max_request=200,意味着每个Worker处理200个请求后重启。如果每个请求加载了框架(如Laravel),200次后重启反而比PHP-FPM还慢。建议设置为5000-10000,配合OPcache。

坑5:PHP-FPM 与 Swoole 混合部署的单测问题

本地开发用php artisan serve (PHP-FPM),线上用Swoole。代码中$_SERVER字段不同,导致某些中间件出错。务必封装统一的请求获取方式,例如request()->server()

总结

没有银弹。PHP-FPM依然是低并发场景的最简单选择。Swoole适合需要WebSocket或愿意拥抱协程的团队。RoadRunner适合想要无痛升级性能的Laravel/Symfony项目——我最终把那个API网关迁移到了RoadRunner,零代码改动,QPS从380提升到1420,内存稳定在400MB(4个worker)。

最后给出我的压测脚本,方便你复现:

# 安装wrk
wrk -t4 -c100 -d30s --latency http://localhost:8080/user/1

# 安装Swoole
pecl install swoole-5.1.2

# RoadRunner二进制下载
curl -sS https://get.rr.dev/cli/install | bash