Что такое Swoole и OpenSwoole

Swoole и OpenSwoole — это не фреймворки в стиле Laravel или Symfony, а PHP-расширения и runtime для долгоживущих сетевых приложений. Они добавляют в PHP встроенные HTTP, WebSocket, TCP/UDP-серверы, coroutine runtime, таймеры, process pool, task workers, shared memory-структуры и coroutine-friendly клиенты.

Практически это означает: PHP-процесс не обязан жить только один request, как в классической модели PHP-FPM. Он может поднять сервер, держать соединения, принимать события и обслуживать много I/O-bound задач внутри одного набора воркеров. Поэтому Swoole/OpenSwoole логически стоят рядом с Асинхронный PHP и event loop, WebSocket, SSE и realtime в PHP и PHP-FPM, RoadRunner и долгоживущие воркеры.

Исторически Swoole и OpenSwoole тесно связаны, но в живых проектах лучше относиться к ним как к отдельным дистрибутивам с похожей моделью. В коде это обычно видно по namespace: Swoole\* или OpenSwoole\*. Нельзя слепо копировать пример из одной документации в проект на другой сборке: проверьте установленное расширение, namespace, версию и доступные compile flags.

Быстрое повторение
Почему Swoole/OpenSwoole нельзя понимать как фреймворк уровня Laravel или Symfony?

Модель выполнения

В PHP-FPM web server передаёт запрос в FPM worker, приложение обрабатывает request, отдаёт response, а затем request-scoped состояние должно исчезнуть. В Swoole/OpenSwoole сам PHP-процесс может быть сервером. Он стартует один раз, создаёт master/manager/worker-процессы, слушает порт и вызывает ваши callbacks при событиях: Request, Open, Message, Close, Receive, Task, Finish.

flowchart TD A[Reverse proxy / client] --> B[OpenSwoole или Swoole server] B --> C[Master process] C --> D[Manager process] D --> E[Worker process 1] D --> F[Worker process 2] D --> G[Task workers] E --> H[Coroutine: HTTP request] E --> I[Coroutine: DB / HTTP I/O] F --> J[Coroutine: WebSocket connection] H --> K[Response] I --> K J --> L[push frame] E -. shared only via special structures .-> M[Table / Atomic / IPC / Redis] F -. shared only via special structures .-> M
flowchart TD
    A[Reverse proxy / client] --> B[OpenSwoole или Swoole server]
    B --> C[Master process]
    C --> D[Manager process]
    D --> E[Worker process 1]
    D --> F[Worker process 2]
    D --> G[Task workers]
    E --> H[Coroutine: HTTP request]
    E --> I[Coroutine: DB / HTTP I/O]
    F --> J[Coroutine: WebSocket connection]
    H --> K[Response]
    I --> K
    J --> L[push frame]
    E -. shared only via special structures .-> M[Table / Atomic / IPC / Redis]
    F -. shared only via special structures .-> M
Упрощённая модель Swoole/OpenSwoole: сервер живёт дольше request, воркеры держат coroutines, а обычная PHP-память не является общей между процессами.

Внутри worker могут создаваться coroutines. Coroutine похожа на очень лёгкий поток userland-уровня: она может приостановиться на ожидании I/O и дать runtime выполнить другую coroutine. Но, как и в предыдущей статье про event loop, это cooperative concurrency. CPU-heavy цикл, неподходящая блокирующая функция или синхронный клиент базы всё равно способны остановить worker.

Быстрое повторение
Что принципиально меняется в lifecycle PHP-приложения при переходе от PHP-FPM к Swoole/OpenSwoole?

Минимальный HTTP-сервер

Пример ниже показывает не «роутер», а саму идею: файл запускается из CLI и остаётся жить как сервер.

<?php

use OpenSwoole\Http\Request;
use OpenSwoole\Http\Response;
use OpenSwoole\Http\Server;
use OpenSwoole\Coroutine;

$server = new Server('127.0.0.1', 9501);

$server->set([
    'worker_num' => 2,
    'enable_coroutine' => true,
]);

$server->on('Start', function (Server $server): void {
    echo "HTTP server: http://127.0.0.1:9501\n";
});

$server->on('Request', function (Request $request, Response $response): void {
    // Имитация неблокирующего ожидания внутри coroutine.
    Coroutine::sleep(0.05);

    $response->header('Content-Type', 'application/json; charset=utf-8');
    $response->end(json_encode([
        'path' => $request->server['request_uri'] ?? '/',
        'worker' => posix_getpid(),
        'time' => date(DATE_ATOM),
    ], JSON_UNESCAPED_SLASHES));
});

$server->start();

Для Swoole namespace будет Swoole\Http\Server, Swoole\Coroutine и так далее. В реальном приложении над таким сервером часто ставят reverse proxy, а фреймворк загружают один раз при старте worker. Это ускоряет bootstrap, но приносит главный риск долгоживущих процессов: stale state.

Быстрое повторение
В минимальном HTTP-сервере Swoole/OpenSwoole почему файл запускают из CLI, а не кладут под обычный PHP-FPM request?

Coroutine runtime и hooks

OpenSwoole-документация подчёркивает удобство coroutine-подхода: код выглядит почти синхронно, но runtime переключает coroutines на ожидании I/O. Это не то же самое, что yield в генераторах и не то же самое, что Fiber сам по себе. Fiber в PHP даёт низкоуровневую возможность приостановки стека; Swoole/OpenSwoole добавляют вокруг этого сетевой runtime, серверы, scheduler и набор I/O API.

Есть два практических стиля:

<?php

use OpenSwoole\Coroutine;
use OpenSwoole\Coroutine\Channel;

Coroutine\run(function (): void {
    $channel = new Channel(1);

    Coroutine::create(function () use ($channel): void {
        Coroutine::sleep(0.1);
        $channel->push('profile loaded');
    });

    echo $channel->pop() . PHP_EOL;
});

Первый стиль — явно пользоваться coroutine API: Coroutine::create(), Coroutine::sleep(), Coroutine\Channel, coroutine clients. Второй — включать runtime hooks, чтобы часть привычных PHP-функций или расширений в coroutine context работала неблокирующе. Hooks удобны, но их нельзя считать магической совместимостью со всем кодом: проверяйте конкретные функции, драйверы и расширения.

WebSocket, TCP и таймеры

Swoole/OpenSwoole особенно полезны там, где PHP должен держать соединение, а не просто быстро отдать HTML. WebSocket-сервер получает события открытия соединения, входящего frame и закрытия. На каждое соединение есть file descriptor, по которому сервер может отправить сообщение обратно.

<?php

use OpenSwoole\WebSocket\Frame;
use OpenSwoole\WebSocket\Server;

$server = new Server('127.0.0.1', 9502);

$server->on('Open', function (Server $server, $request): void {
    echo "open: {$request->fd}\n";
});

$server->on('Message', function (Server $server, Frame $frame): void {
    $server->push($frame->fd, 'echo: ' . $frame->data);
});

$server->on('Close', function (Server $server, int $fd): void {
    echo "close: {$fd}\n";
});

$server->start();

Таймеры (tick, after, clear) нужны для heartbeat, периодической очистки, отложенных действий, проверки состояния соединений. Важно не превращать timer callback в тяжёлую задачу. Для долгой CPU-bound работы лучше использовать task workers, отдельные процессы или обычные Очереди, фоновые задачи и воркеры.

TCP/UDP-серверы находятся уровнем ниже HTTP. Они подходят для кастомных протоколов, IoT, внутренних бинарных протоколов, игровых или realtime-шлюзов. Цена — вы сами отвечаете за framing, timeouts, backpressure и протокол ошибок.

Channels, shared state и процессы

Есть два разных смысла «shared».

Первый — shared state внутри одного worker-процесса. Coroutines разделяют память процесса: объекты, singleton’ы, статические свойства, контейнер, кеши. Это быстро, но опасно. Если coroutine A положила текущего пользователя в глобальный сервис, затем уступила управление на I/O, coroutine B может увидеть или изменить это состояние. Request data должны быть локальными для request/coroutine, а не лежать в случайном singleton.

<?php

final class CurrentUser
{
    public static ?int $id = null;
}

// Плохо для coroutine runtime: это состояние разделяет весь worker.
CurrentUser::$id = $requestUserId;
Coroutine::sleep(0.05);

// За время ожидания другая coroutine могла записать сюда другого пользователя.
doSomethingFor(CurrentUser::$id);

Второй — обмен между процессами. Worker-процессы не делят обычную PHP-память. Для координации нужны специальные механизмы: Table, Atomic, locks, process pool, task workers, IPC-каналы или внешнее хранилище вроде Redis. Coroutine\Channel — это в первую очередь очередь синхронизации между coroutines; не надо считать её универсальной заменой брокера сообщений.

Отличие от PHP-FPM

Главное отличие не в скорости, а в lifecycle. В PHP-FPM плохой singleton чаще всего умирает вместе с request. В Swoole/OpenSwoole он может жить часами. Это меняет требования к контейнеру приложения, middleware, подключению к базе, кешам, логированию, locale, timezone, current tenant и current user.

Типовые правила такие:

  • bootstrap можно делать один раз, но request-specific данные нужно сбрасывать на каждый request;
  • соединения к БД и Redis надо проверять на разрыв и reconnect;
  • нельзя хранить Request, пользователя или DTO ответа в статических свойствах;
  • memory leaks становятся production-проблемой, а не мелкой небрежностью;
  • graceful reload, лимиты памяти и health checks обязательны, а не «потом добавим».

Поэтому Swoole/OpenSwoole редко стоит выбирать только потому, что «быстрее». Они уместны для realtime, большого fan-out к внешним сервисам, high-concurrency I/O, WebSocket gateway, TCP-сервисов и приложений, где выигрыш от долгоживущего runtime превышает эксплуатационную сложность. Для обычного CRUD через PHP-FPM проще, предсказуемее и дешевле поддерживать классическую модель.

См. также

Источники

  1. Open Swoole Coroutine
  2. Open Swoole HTTP Server
  3. OpenSwoole WebSocket Server onMessage
  4. OpenSwoole Timer
  5. OpenSwoole Coroutine Channel stats
  6. Swoole official documentation
  7. PHP Manual: Swoole
  8. OpenSwoole GitHub repository