PHP 异步编程与常驻内存:Swoole、ReactPHP 与协程并发实战

从传统 PHP-FPM 的阻塞模型讲起,系统介绍 PHP 异步编程的三大路线:Swoole 协程与 HTTP/TCP 服务器、ReactPHP 纯 PHP 事件循环、以及 FrankenPHP 常驻 Worker 模式,并给出协程并发控制、连接池、任务队列的完整实战代码与选型建议。

引言

传统 PHP 的编程模型是「一个请求一个进程」:PHP-FPM 为每个请求新建/复用 worker,请求结束即释放一切。这种模型简单、安全、易于横向扩展,但对长连接、高并发 IO、实时推送这类场景却力不从心——每个 worker 阻塞等待 IO 时,进程就白占着内存。

PHP 异步与常驻内存技术正是为填补这个空白而生:Swoole 用协程把「并发 IO」变成同步写法;ReactPHP 用纯 PHP 事件循环实现非阻塞 IO;FrankenPHP 把 PHP 打进 Caddy,用 Worker 模式常驻。本文将从阻塞模型讲起,手写三个路线的可运行示例,最后给出协程并发、连接池与任务队列的生产级写法,帮你判断「我该不该上异步」。

关联阅读:https://plumephp.com/php-performance-tuning/ 里的 FPM 调优是异步化的前置;https://plumephp.com/php8-modern-features/ 的 Fibers 是 Swoole 协程的底层基础之一。


目录


1. 从阻塞到异步:为什么需要常驻内存

1.1 阻塞模型的问题

传统 FPM 处理 3 个慢请求:
worker1: [DB查询 200ms] [渲染] [返回]      ← 全程独占
worker2: [HTTP调用 300ms] [渲染] [返回]
worker3: [Redis 5ms] [返回]
└ 并发能力 = worker 数;每个 worker 在 IO 等待时闲置

IO 等待占请求时长的绝大部分,而 worker 在等待时空转占用内存。

1.2 常驻模型的目标

常驻进程(协程)处理 N 个请求:
协程A: [DB查询] ──挂起──▶ [拿到结果继续]
协程B: [HTTP调用] ──挂起──▶ [继续]
协程C: [Redis] ──▶ [立即返回]
└ 一个进程内,IO 等待时切换到其他协程,利用率拉满

1.3 三条路线定位

方案本质最擅长
Swoole协程 + 事件循环,进程内并发高性能 HTTP/TCP/WS
ReactPHP纯 PHP 事件循环库中间层、CLI、流式 IO
FrankenPHPCaddy 内置 PHP,Worker 常驻现代框架无痛部署

2. 协程:用同步的写法做并发

2.1 协程是什么

协程(Coroutine)是「可暂停/恢复的函数」:碰到 IO 阻塞时主动让出 CPU,IO 完成后从暂停点继续。Swoole 在 PHP 层实现了协程调度器,代码却保持同步风格。

2.2 同步 vs 协程对比

// 同步阻塞:串行等待
$users = [];
foreach ($ids as $id) {
    $users[] = $db->query("SELECT * FROM users WHERE id=$id");  // 逐个等
}

// Swoole 协程:并行发起
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;

$ch = new Channel(count($ids));
foreach ($ids as $id) {
    Coroutine::create(function () use ($db, $id, $ch) {
        $ch->push($db->query("SELECT * FROM users WHERE id=$id"));
    });
}
$users = [];
for ($i = 0; $i < count($ids); $i++) {
    $users[] = $ch->pop();   // 按序收结果,但并发执行
}

2.3 协程 vs 多进程

维度协程多进程
内存共享,低每进程一份
切换开销用户态极低内核调度
编程模型同步写法需 IPC
多核利用需配合多 worker天然

生产实践:每核心 1 个 worker 进程,进程内多协程并发。


3. Swoole 入门:HTTP 与 WebSocket 服务器

3.1 安装

pecl install swoole
php -m | grep swoole   # 确认扩展加载

3.2 HTTP 服务器

<?php
// http_server.php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;

$server = new Server('0.0.0.0', 9501);
$server->set([
    'worker_num' => 4,          // 4 worker 进程
    'max_conn'   => 10000,
]);

$server->on('request', function (Request $req, Response $res) {
    $res->header('Content-Type', 'application/json');
    $res->end(json_encode(['path' => $req->server['request_uri']]));
});

$server->start();
php http_server.php          # 常驻
curl http://127.0.0.1:9501/hello

3.3 WebSocket 实时推送

use Swoole\WebSocket\Server;

$ws = new Server('0.0.0.0', 9502);

$ws->on('open', function ($server, $req) {
    echo "client connected: {$req->fd}\n";
});

$ws->on('message', function ($server, $frame) {
    // 群发:把消息推给所有连接
    foreach ($server->connections as $fd) {
        $server->push($fd, "echo: {$frame->data}");
    }
});

$ws->start();

3.4 与 FPM 协同

Swoole 可以只做「实时层」,业务读写仍走 PHP-FPM;也可以完全替代 FPM 直接跑框架(配合框架的 Swoole 适配器)。


4. Swoole 协程并发控制与超时

4.1 并发上限控制

并发请求太多会打爆下游,需要限流:

use Swoole\Coroutine;
use Swoole\Coroutine\Semaphore;

$sem = new Semaphore(10);   // 最多 10 个同时进行

Coroutine::create(function () use ($sem, $task) {
    $sem->wait();           // 拿不到信号量就挂起
    try {
        $result = $http->get($task->url);
    } finally {
        $sem->post();       // 释放
    }
});

4.2 超时保护

use Swoole\Coroutine\Http\Client;

$client = new Client('api.example.com', 443, true);
$client->set(['timeout' => 2.0]);   // 2 秒超时
$client->get('/v1/orders');
if ($client->errCode === 110) {
    // 超时处理:降级或重试
}

4.3 协程并行等待(并发聚合)

use Swoole\Coroutine\WaitGroup;

$wg = new WaitGroup();
$results = [];

foreach (['user', 'orders', 'recommend'] as $key) {
    $wg->add();
    Coroutine::create(function () use ($wg, $key, &$results) {
        try {
            $results[$key] = callService($key);
        } finally {
            $wg->done();
        }
    });
}
$wg->wait();   // 等待全部完成
// $results 已就绪

这就是「BFF 聚合多个下游接口」的协程版实现。


5. ReactPHP:纯 PHP 事件循环

5.1 安装与最小事件循环

composer require react/event-loop react/http
<?php
// async_server.php
require __DIR__.'/vendor/autoload.php';

use React\EventLoop\Loop;
use React\Http\Server;
use Psr\Http\Message\ServerRequestInterface;

$server = new Server(function (ServerRequestInterface $request) {
    return new React\Http\Message\Response(
        200,
        ['Content-Type' => 'application/json'],
        json_encode(['hello' => 'reactphp'])
    );
});

$socket = new React\Socket\SocketServer('127.0.0.1:8080');
$server->listen($socket);
echo "Listening on 8080\n";
Loop::run();   // 启动事件循环,常驻

5.2 非阻塞延迟任务

use React\EventLoop\Loop;

// 定时任务(无需 crontab)
Loop::addPeriodicTimer(5.0, function () {
    echo "每 5 秒执行\n";
});

5.3 ReactPHP 的定位

它不提供协程(PHP 7 时代),而是用「回调/Promise」风格。现代选择通常倾向 Swoole 或 FrankenPHP;ReactPHP 更适合轻量 CLI 工具与中间件。


6. FrankenPHP:常驻 Worker 的现代部署

6.1 它是什么

FrankenPHP 把 PHP 直接编译进 Caddy Web 服务器,用 Worker 模式让 PHP 应用常驻内存,无需额外 FPM。

6.2 Caddyfile 配置

# Caddyfile
:8080 {
    root * /app/public
    php_server {
        worker /app/public/index.php 4
    }
}

worker 启动 4 个常驻 worker,Laravel/Symfony 直接跑在内存里。

6.3 一个请求的生命

Caddy 收请求 → 交给常驻 worker(已加载框架)
→ 框架处理(无需重建容器/连接) → 返回

6.4 Docker 部署

FROM dunglas/frankenphp
COPY --from=composer /app/vendor /app/vendor
COPY . /app
WORKDIR /app
RUN frankenphp php-cli artisan config:cache
CMD ["frankenphp", "run"]

6.5 FrankenPHP vs Swoole

维度FrankenPHPSwoole
上手极简(Caddy 原生)需学协程 API
框架兼容Laravel 原生需适配器
协程无(靠 Worker 并发)有
定位替代 FPM 的部署升级高性能定制服务

7. 连接池与任务队列实战

7.1 数据库连接池(Swoole 协程)

常驻进程必须复用连接,否则每请求建连反而更慢:

use Swoole\Coroutine\Channel;
use Swoole\Coroutine;

class MysqlPool
{
    private Channel $pool;

    public function __construct(int $size = 10)
    {
        $this->pool = new Channel($size);
        for ($i = 0; $i < $size; $i++) {
            $this->pool->push($this->newConnection());
        }
    }

    private function newConnection() { /* 返回协程版 PDO 连接 */ }

    public function get()
    {
        return $this->pool->pop(2.0);   // 2 秒内拿不到连接则失败
    }

    public function put($conn): void
    {
        $this->pool->push($conn);
    }
}

7.2 任务队列消费

// 从 Redis 队列消费,用协程并发处理
use Swoole\Coroutine;

Coroutine::create(function () {
    while (true) {
        $task = $redis->brPop(['task:queue'], 1);
        if (!$task) continue;

        Coroutine::create(fn () => processTask($task[1]));
    }
});

7.3 信号处理与优雅退出

$server->on('shutdown', function ($server) {
    // 关闭连接池、刷日志、保存状态
});

8. 选型决策:什么时候上异步

8.1 该上异步的信号

场景传统 FPM 的困境异步的解法
WebSocket 实时推送worker 被长连接占死Swoole/WS 常驻
BFF 聚合多个接口串行等待慢协程并行
高并发短请求进程数受限单进程协程
大量后台任务靠队列外挂常驻 worker 消费

8.2 不该上异步的信号

  • 流量不高、部署简单 → FPM + OPcache 足够。
  • 团队不熟悉协程/事件循环模型 → 维护成本高。
  • 依赖阻塞式 C 扩展 → 无法协程化。

8.3 渐进式迁移建议

  1. 先把「实时推送」独立成 Swoole 微服务,FPM 继续跑常规 Web。
  2. 再把「BFF 聚合」「后台任务」迁到常驻。
  3. 最后评估是否整体替换 FPM(框架适配成熟度决定)。

不要为了异步而异步——先量化瓶颈,再选工具。


9. 总结与踩坑清单

9.1 常驻内存的典型坑

坑说明对策
全局变量残留常驻进程里全局变量跨请求保留用请求作用域容器
单例持有连接连接断开未重建连接池 + 健康检查
协程内用阻塞函数阻塞整个 worker用协程版客户端
内存泄漏循环引用/大缓存不释放定期重启 worker
与 FPM 语义差异die()、header() 行为不同用框架适配层

9.2 关键结论

  • 常驻内存解决「重建开销 + IO 等待」,不是魔法加速。
  • 协程让并发代码像同步一样好读,但必须用协程安全的客户端。
  • 从最小切口开始:先把实时/聚合场景异步化,其余保持简单。

延伸阅读

  • https://plumephp.com/php-performance-tuning/ — FPM/OPcache/JIT 的调优基础
  • https://plumephp.com/php8-modern-features/ — Fibers 与语言层面的并发底座
  • Swoole 官方文档 与 FrankenPHP 项目主页

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「php」更多文章

  1. PHP 面向对象与设计模式:SOLID、常用模式与 Laravel 实践
  2. PHP 静态分析与代码质量:PHPStan、Psalm、Rector 与 CI 门禁
  3. PHP 部署运维实战:Nginx、PHP-FPM、Docker 与 CI/CD