PHP на сервере: FPM, Swoole, воркеры и event loop
В учебниках часто остаётся картинка «залил index.php на хостинг», а в продакшене PHP почти всегда — это менеджер процессов + веб‑сервер. Здесь разведены четыре идеи, которые обычно смешивают:
- Классический запрос/ответ — PHP-FPM (или
mod_php): короткий запрос на воркер, затем очистка или контролируемое переиспользование. - Долгоживущие приложения — Swoole/OpenSwoole, RoadRunner, FrankenPHP (worker) — один процесс PHP обслуживает много запросов; статические кеши и глобальное состояние становятся реальной проблемой.
- Библиотеки с event loop — ReactPHP, AMPHP, Revolt: кооперативная многозадачность; хорошо для I/O, плохо с блокирующими вызовами.
- CLI —
phpдля cron, очередей, миграций — тот же язык, другие ограничения.
Это не «новый PHP», а разные контракты хостинга. Выбор зависит от профиля нагрузки, команды и готовности к эксплуатации.
Содержание
- PHP-FPM + nginx (или Apache как прокси)
- Apache
mod_php - CLI PHP
- Swoole / OpenSwoole / Laravel Octane
- RoadRunner
- FrankenPHP
- Event loop: ReactPHP, AMPHP, Revolt
- Сравнение: когда что уместно
- Утечки памяти: общий чеклист
PHP-FPM + nginx (или Apache как reverse proxy)
Как это устроено
- nginx завершает TLS и отдаёт статику.
- Для
*.phpзапрос уходит в PHP-FPM по FastCGI (сокет или TCP). - FPM выдаёт воркер из пула. Он выполняет bootstrap (
public/index.phpв Laravel), отвечает и возвращается в пул (или завершается после N запросов — см.pm.max_requests).
На один запрос не стоит рассчитывать, что глобальные переменные «переживут» следующий HTTP‑запрос (даже если Opcache держит байткод тёплым).
Плюсы
- Проверенная связка с Laravel, Symfony, WordPress.
- Простая модель: запрос пришёл — ответ ушёл; память освобождается при перезапуске воркера.
- Горизонтальное масштабирование: больше воркеров и серверов за балансировщиком.
- Меньше сюрпризов с типичными Composer‑пакетами.
Минусы
- Стоимость bootstrap на запрос (смягчается Opcache, preload при тюнинге).
- Параллелизм = число воркеров; при перегрузе очередь растёт в FPM — важно настроить
pm.*. - Не лучший вариант для миллионов долгих WebSocket на одной машине без дополнительного слоя.
Рецепт (стиль Ubuntu)
sudo apt update
sudo apt install php8.5-fpm
sudo systemctl enable --now php8.5-fpm
Фрагмент nginx:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
}
Пул /etc/php/8.5/fpm/pool.d/www.conf (подстройте под RAM):
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
sudo systemctl reload php8.5-fpm
Память (FPM)
pm.max_requests— периодический перезапуск воркера; дешёвая страховка от редких утечек в расширениях.- Если RSS воркеров ползёт вверх — профилируйте код и расширения; FPM только смягчает симптомы.
Apache mod_php
PHP встроен в процессы/потоки Apache. По сути тоже запрос/ответ, но другая модель изоляции и тюнинга.
Плюсы
- Просто на одном сервере, наследие shared hosting.
Минусы
- Жизненный цикл PHP связан с воркерами Apache; меньше гибкости, чем у nginx + FPM в типичных Laravel‑деплоях.
Когда уместно: легаси или осознанный выбор команды; для новых Laravel‑проектов чаще выбирают FPM + nginx.
CLI PHP (cron, очереди, Artisan)
php artisan ..., консюмеры очередей, планировщик — без HTTP.
Плюсы
- Идеально для пакетов, импорта, переиндексации.
Минусы
- Не заменяет веб‑SAPI; другие таймауты и окружение.
Рецепт
cd /var/www/app
php artisan queue:work redis --sleep=1 --tries=3
Долгоживущие воркеры очередей = мини‑серверы: применяйте чеклист для долгоживущих процессов.
Swoole / OpenSwoole / Laravel Octane
Swoole и OpenSwoole — встроенный долгоживущий сервер: воркеры живут долго, обрабатывают много запросов, могут использовать корутины для конкурентного I/O.
Laravel Octane может использовать Swoole/RoadRunner/FrankenPHP: один раз загрузили фреймворк, много запросов.
Плюсы
- Высокий throughput для I/O‑bound при «согласующемся» коде.
- WebSocket, таймеры, асинхронные примитивы (при корутинно‑совместимых API).
Минусы
- Глобальное состояние переживает запросы —
static, синглтоны, кеши в памяти. - Не все пакеты безопасны (скрытый блокирующий I/O, предположения про сессии).
- Сложнее деплой и отладка — после выката нужен reload воркеров.
Рецепт (минимальный пример Swoole)
pecl install swoole # либо пакет php8.5-swoole в дистрибутиве
<?php
$http = new Swoole\Http\Server('127.0.0.1', 9501);
$http->on('request', function ($request, $response) {
$response->header('Content-Type', 'text/plain; charset=utf-8');
$response->end('ok');
});
$http->start();
Octane (Laravel)
composer require laravel/octane
php artisan octane:install
php artisan octane:start
Память
- Настройте перезапуск воркеров (см. актуальные опции Octane/Swoole для вашей версии).
- Не кладите данные конкретного пользователя в статические поля.
- После деплоя — graceful reload (systemd,
octane:reloadи т.д.).
RoadRunner
RoadRunner — бинарник на Go, держит PHP‑воркеры живыми; связь через goridge (часто вместе с Octane или пакетами Spiral).
Плюсы
- Сильная история надзора за воркерами; Go‑слой обслуживает HTTP, gRPC, очереди.
Минусы
- Ещё один компонент в релизе (RR + конфиг).
- Те же риски персистентного состояния, что у Swoole.
Рецепт
Скачайте бинарник с релизов RoadRunner, положите .rr.yaml и запускайте:
./rr serve -c .rr.yaml
Команда воркера задаётся в конфиге (часто через Octane или сгенерированный worker).
FrankenPHP
FrankenPHP — сервер приложений на базе Caddy с режимом worker (долгий PHP на многие запросы) и современным HTTP‑стеком.
Плюсы
- Удобный single binary сценарий с Caddy; интересен для edge и worker‑режима.
Минусы
- Экосистема новее — проверяйте матрицу совместимости Laravel/Octane и расширений.
Следуйте официальным гайдам FrankenPHP и варианту установки через Octane.
Event loop: ReactPHP, AMPHP, Revolt
ReactPHP и AMPHP (на Revolt / amphp/amp) дают однопоточный event loop и неблокирующий I/O если вы вызываете их API.
Плюсы
- Отлично для I/O‑bound задач: много сокетов, HTTP‑клиенты, DNS, таймеры.
- Удобно для кастомных протоколов и интеграционных «мостов».
Минусы
- Любой блокирующий вызов (
sleep(), синхронный PDO к удалённой БД,file_get_contents('http://...')) стопорит весь цикл. - Нужны async‑клиенты или вынос блокирующей работы в пул процессов/потоков.
Рецепт (набросок AMPHP)
composer require amphp/http-client revolt/event-loop
<?php
require __DIR__ . '/vendor/autoload.php';
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
$client = HttpClientBuilder::buildDefault();
$futures = [];
foreach (['https://example.com', 'https://php.net'] as $url) {
$futures[] = async(fn () => $client->request(new Request($url)));
}
foreach ($futures as $future) {
echo $future->await()->getStatus(), "\n";
}
Рецепт (набросок ReactPHP)
composer require react/event-loop react/http
<?php
require __DIR__ . '/vendor/autoload.php';
$loop = React\EventLoop\Loop::get();
$loop->addPeriodicTimer(1.0, fn () => print "tick\n");
$loop->run();
Сравнение: когда что уместно
| Среда | Хорошо подходит | Обычно не стоит | |--------|-----------------|-----------------| | PHP-FPM | Типичные Laravel/Symfony сайты и API | Огромное число дешёвых duplex‑соединений на одной ноде без доп. слоя | | Swoole / Octane | Высокий QPS, WebSocket, корутинный I/O | Команда не готова к персистентному состоянию; много блокирующих библиотек | | RoadRunner | Строгий надзор за воркерами, несколько протоколов | Нельзя тащить ещё один бинарник в деплой | | FrankenPHP | Caddy‑центричный деплой, worker‑режим | Нужен максимально консервативный стек | | ReactPHP / AMPHP | Свои сетевые сервисы, async‑клей | Классический CRUD с блокирующим стеком фреймворка |
Утечки памяти: общий чеклист
- Статика и синглтоны — только конфигурация, не данные запроса.
- Кеши без лимита — лучше Redis/Memcached или LRU в памяти с потолком.
- Замыкания, удерживающие большие графы объектов.
- Таймеры и подписки — отменяйте при завершении работы.
- Большие выборки из БД — читайте порциями.
- Opcache не лечит утечки — используйте перезапуск воркеров (
pm.max_requests, reload Octane).
Наблюдение:
ps aux | grep php-fpm
В коде:
<?php
echo memory_get_usage(true), " bytes\n";