Класичний PHP-FPM запускає інтерпретатор заново на кожен запит: підняв застосунок, обробив один запит, вивантажив усе з пам'яті. Swoole і RoadRunner працюють інакше — застосунок завантажується один раз і тримається в пам'яті між запитами, як у Node.js або Go. Це пришвидшує обробку в рази, але вимагає іншого підходу до коду.
Чим це відрізняється від звичного PHP-FPM
У моделі FPM кожен запит отримує чистий стан: жодних витоків між користувачами, але щоразу заново ініціалізується контейнер залежностей, з'єднання з базою і автозавантаження класів. У моделі Swoole або RoadRunner ініціалізація відбувається один раз при старті воркера, а далі на кожен запит виконується лише код обробника — це і дає основний приріст швидкості. Різницю зі звичайним CLI-запуском PHP розбирали у статті про PHP CLI і FPM.
Зворотний бік — глобальний стан. Статичні властивості класів, синглтони і просто забуті змінні в замиканнях живуть між запитами і можуть текти в пам'ять або протікати даними одного користувача до іншого, якщо код не написаний із розрахунком на довгий процес.
Встановлення Swoole як розширення PHP
sudo apt install php8.3-dev php-pear
sudo pecl install swoole
echo "extension=swoole.so" | sudo tee /etc/php/8.3/mods-available/swoole.ini
sudo phpenmod swoole
php -m | grep swoole
Swoole вбудовується прямо в PHP як розширення і додає класи для асинхронного HTTP-сервера, корутин і вебсокетів. Про окремий протокол вебсокетів на чистому PHP — у статті про WebSockets із Ratchet.
RoadRunner як альтернатива
RoadRunner влаштований інакше: це окремий бінарник на Go, який тримає
пул PHP-воркерів і спілкується з ними за протоколом Goridge через
stdin/stdout. PHP-код при цьому лишається звичайним PHP без спеціального
розширення — потрібен лише пакет spiral/roadrunner-http через
Composer, який описано у статті
про встановлення Composer.
| Параметр | Swoole | RoadRunner |
|---|---|---|
| Тип | розширення PHP | окремий бінарник на Go |
| Корутини | так, вбудовані | ні, воркери однопотокові |
| Сумісність з Laravel/Symfony | через окремі пакети | через окремі пакети |
| Поріг входу | вищий | нижчий |
На що дивитись при витоках пам'яті
Довгоживучий процес робить витоки помітними за хвилини, а не за тижні, як у звичайних десктопних застосунках. Перевірте ці місця:
- Статичні масиви і кеші рівня класу, які ростуть з кожним запитом і ніколи не очищаються.
- З'єднання з базою даних, яке переідкривається на кожен запит замість повторного використання вже відкритого з'єднання.
- Глобальний контейнер залежностей — якщо він створюється заново на кожен запит, то втрачається весь сенс довгоживучого процесу.
- Циклічні посилання між об'єктами, які збирач сміття PHP не завжди прибирає вчасно у довгому процесі.
Моніторинг воркерів і перезапуск
Навіть правильно написаний застосунок варто перезапускати за
розкладом, так само як і звичайні черги — підхід майже такий самий, як у
статті про Supervisor для
черг. У Swoole є вбудований параметр max_request, який
перезапускає воркер після заданої кількості оброблених запитів:
$server->set([
'worker_num' => 4,
'max_request' => 5000,
'task_worker_num' => 2,
]);
Чек-лист переходу на довгоживучий процес
- Приберіть із коду будь-які статичні властивості, які накопичують дані між запитами без явного очищення.
- Налаштуйте плановий перезапуск воркерів через
max_requestу Swoole або аналогічний ліміт у RoadRunner. - Прожене навантажувальне тестування перед перемиканням проду — зростання затримок за час роботи воркера одразу покаже витік.