PHP на сервері: FPM, Swoole, воркери та event loop
У багатьох підручниках досі фігурує «закинув index.php на хостинг», тоді як у продакшені PHP — це майже завжди менеджер процесів + вебсервер спереду. Тут розведено чотири підходи, які часто змішують:
- Класичний запит/відповідь — PHP-FPM (або
mod_php): короткий життєвий цикл одного HTTP‑запиту на воркер. - Довгоживучі сервери застосунків — Swoole/OpenSwoole, RoadRunner, FrankenPHP (worker): той самий PHP‑воркер обробляє багато запитів; спільна пам’ять і статичні кеші стають реальною проблемою.
- Бібліотеки з event loop — ReactPHP, AMPHP, Revolt: кооперативна багатозадачність; добре для I/O, небезпечно з блокуючими викликами.
- CLI / cron —
phpдля скриптів, черг, міграцій — не веб‑модель, але той самий мова з іншими обмеженнями.
Жоден із варіантів не є «новим PHP» — це різні контракти хостингу. Обирайте за формою трафіку, досвідом команди та готовністю до експлуатації.
Зміст
- PHP-FPM + nginx (або Apache як проксі)
- Apache
mod_php(вбудований) - CLI PHP (cron, воркери, Artisan)
- Swoole / OpenSwoole / Laravel Octane
- RoadRunner
- FrankenPHP
- Event loop: ReactPHP, AMPHP, Revolt
- Порівняння: коли що брати
- Витоки пам’яті: спільний чеклист
PHP-FPM + nginx (або Apache як reverse proxy)
Що відбувається
- nginx завершує TLS і віддає статику.
- Для
*.phpзапит передається в PHP-FPM через FastCGI (Unix‑сокет або TCP). - FPM видає воркер з пулу. Він виконує bootstrap (
public/index.phpу Laravel), відправляє відповідь і повертається в пул (або завершується після N запитів — див.pm.max_requests).
На кожен запит не варто розраховувати, що глобальні змінні «переживуть» наступний HTTP‑запит (навіть якщо Opcache тримає байткод теплим).
Плюси
- Перевірена зв’язка з Laravel, Symfony, WordPress тощо.
- Зрозуміла модель: запит увійшов — відповідь вийшла; пам’ять звільняється при перезапуску воркера.
- Легке горизонтальне масштабування: більше FPM‑воркерів і серверів за балансувальником.
- Менше сюрпризів із типовими Composer‑пакетами.
Мінуси
- Вартість bootstrap на кожен запит (зменшується Opcache, preload у налаштованих середовищах).
- Паралелізм = кількість воркерів; під навантаженням черга росте в backlog 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— перезапуск воркера після N запитів; недорога страховка від дрібних витоків у розширеннях.- Якщо RSS воркерів росте без обмежень — профілюйте код і розширення; FPM лише приховує симптоми.
Apache mod_php (вбудований)
Apache запускає PHP у своїх процесах/потоках (mod_php). Здебільшого це теж межі запиту, але архітектура процесів відрізняється від FPM.
Плюси
- Просто на односерверних сетапах; спадщина shared hosting.
Мінуси
- Життєвий цикл PHP прив’язаний до воркерів Apache — тюнінг і ізоляція відрізняються від nginx + FPM.
- У сучасних Laravel‑деплоях рідше, ніж FPM + nginx.
Коли брати: легасі або свідомий вибір команди; інакше зазвичай FPM + nginx.
CLI PHP (cron, черги, Artisan)
php artisan ..., php bin/console ..., воркери черг, планувальник — без HTTP‑фасаду.
Плюси
- Ідеально для пакетної обробки, черг, переіндексації, імпортів.
Мінуси
- Не замінює веб‑SAPI; інші таймаути, немає семантики nginx‑буферів «на запит».
Рецепт
cd /var/www/app
php artisan schedule:work # зазвичай dev; у проді часто cron -> artisan schedule:run
php artisan queue:work redis --sleep=1 --tries=3
Пам’ять: довгоживучі воркери черг поводяться як малі сервери — застосуйте чеклист для довгоживучих процесів.
Swoole / OpenSwoole / Laravel Octane
Swoole (і форк OpenSwoole) — довгоживучий сервер: воркери живуть довго, обробляють багато запитів і можуть використовувати корутини для конкурентного I/O.
Laravel Octane може керувати Swoole/RoadRunner/FrankenPHP — та сама ідея: завантажити фреймворк один раз, обслужити багато запитів.
Плюси
- Висока пропускна здатність для I/O‑bound коду, який «співпрацює» з моделлю.
- WebSocket, таймери, асинхронні примітиви (за наявності корутинно‑сумісних API).
Мінуси
- Глобальний стан переживає запити —
static, синглтони, кеші можуть «перетікати» між користувачами. - Не всі пакети Composer безпечні (прихований блокуючий I/O, глобалі, припущення щодо
$_SESSION). - Складніші деплой і налагодження — після релізу потрібен reload воркерів.
Рецепт (ілюстративний HTTP‑сервер)
У проді зазвичай Octane або інтеграція у фреймворк; тут показано форму 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 # оберіть swoole/roadrunner/frankenphp
php artisan octane:start
Пам’ять / витоки
- Налаштуйте перезапуск воркерів (див. актуальні опції Octane/Swoole для вашої версії).
- Не зберігайте дані конкретного користувача в статичних полях.
- Після деплою — graceful restart (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 mode (довгий PHP на багато запитів) і сучасними шляхами деплою HTTP/3.
Плюси
- Один бінарник разом із 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, таймери.
- Зручно для власних протоколів, проксі, інтеграційних «мостів».
Мінуси
- Будь-який блокуючий виклик (
PDO::queryдо віддаленої БД типовим драйвером,sleep(),file_get_contents('http://...')) зупиняє цикл для всіх. - Потрібні async‑клієнти (
amphp/http-client, адаптери ReactPHP) або винесення блокуючої роботи в пул потоків/дочірні процеси (складніше).
Рецепт (набросок 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) {
$response = $future->await();
echo $response->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 | Типові сайти та API на Laravel/Symfony | Мільйони дешевих duplex‑з’єднань на одній ноді без додаткового шару | | Swoole / Octane | Високий QPS, WebSocket, корутинний I/O | Команда не готова до персистентного стану; багато блокуючих бібліотек | | RoadRunner | Нагляд за воркерами + кілька протоколів на краю | Немає ресурсу на ще один бінарник у деплої | | FrankenPHP | Caddy‑центричні деплої, експерименти з worker | Потрібен максимально консервативний стек | | ReactPHP / AMPHP | Власні мережеві сервіси, async‑клей | Класичний CRUD із блокуючим стеком фреймворка |
Витоки пам’яті: спільний чеклист (особливо для довгоживучого PHP)
- Статичні властивості й синглтони — лише конфігурація, ніколи дані запиту.
- Глобальні кеші без меж — LRU з обмеженням або Redis/Memcached замість необмежених масивів у PHP.
- Замикання, що утримують великі графи об’єктів.
- Таймери / підписки — скасовуйте періодичні таймери; знімайте слухачі при завершенні.
- Результати з БД — читайте порціями; не накопичуйте величезні масиви в пам’яті.
- Opcache не лікує витоки — перезапускайте воркери (
pm.max_requests, reload Octane), щоб зменшити дрейф на рівні розширень.
Спостереження:
# FPM: стежте за RSS воркерів під навантажувальним тестом
ps aux | grep php-fpm
У коді:
<?php
echo memory_get_usage(true), " bytes\n";