Запуск в продакшене
Запуск Rapira на сервере требует того, без чего локальная команда rapira serve app/worker.php обходится: старта при загрузке машины, возврата после падения, перезагрузки нового кода без потери запросов и логов, которые потом можно прочитать. Эта страница описывает юнит systemd, место для конфигурации, прокси перед сервером и настройки, которые ставят ограничения долгоживущим воркерам.
Почти ничего из этого в бинарник не вшито. Ничто в Rapira не зависит от того, где лежит ваша конфигурация и что управляет процессом, так что раскладка ниже — соглашение, которое вводит эта страница и на которое опирается остальная документация. Сначала доставьте бинарник на машину, об этом рассказывает Установка.
Юнит systemd
Rapira встаёт на место php-fpm, а за пулом уже присматривает её мастер-процесс: он форкает воркеры, дожидается завершившихся, поднимает замену с нарастающей паузой, обновляет отработавшие своё и масштабирует пул. У systemd остаётся одна задача — следить, чтобы этот единственный мастер-процесс был жив, поэтому отдельному менеджеру процессов вроде supervisord тут делать нечего.
Пакеты .deb и .rpm кладут на машину бинарник и встроенный в него рантайм PHP, и больше ничего: ни юнита службы, ни php.ini (точный список файлов есть в разделе Установка). И то и другое относится к политике конкретной установки, а пакет, который принёс бы их с собой, при каждом обновлении затирал бы ваши правки.
Напишите юнит сами, в файле /etc/systemd/system/rapira.service:
[Unit]
Description=Rapira PHP application server
After=network.target
[Service]
Type=exec
WorkingDirectory=/srv/app
ExecStart=/usr/bin/rapira serve --config /etc/rapira/rapira.toml
ExecReload=/bin/kill -USR2 $MAINPID
KillMode=mixed
Restart=on-failure
RuntimeDirectory=rapira
Environment=PHPRC=/etc/rapira
[Install]
WantedBy=multi-user.targetДальше остаётся перечитать юниты и включить службу:
sudo systemctl daemon-reload
sudo systemctl enable --now rapiraШесть строк в нём требуют пояснения:
Type=exec— Rapira работает на переднем плане и никогда не уходит в фон сама. Режима демона нет, и он не нужен: процесс, который запускает systemd, и есть мастер-процесс, поэтому в$MAINPIDлежит ровно тот pid, которому вы собираетесь слать сигналы.ExecReload— превращаетsystemctl reload rapiraвSIGUSR2мастер-процессу, то есть в ту самую перезагрузку без простоя, о которой ниже.KillMode=mixed— по умолчанию systemd отправляет сигнал остановки всем процессам в cgroup, а воркер понимаетSIGTERMкак команду умереть немедленно. Сmixedсигнал получает только мастер-процесс, а он уже запускает мягкое завершение черезSIGQUIT, описанное ниже;SIGKILLпо истеченииTimeoutStopSecпри этом по-прежнему достаётся всей группе. Без этой строкиsystemctl stopиsystemctl restartобрывают запросы на полпути.Restart=on-failure— при штатной остановке процесс завершается с нулевым кодом и остаётся выключенным, поэтому эта строка поднимает сервер только после падения или неудачного старта.RuntimeDirectory=rapira— systemd создаёт/run/rapiraпри старте и убирает при остановке. Именно там лежат pid-файл и Unix-сокет из примеров ниже.Environment=PHPRC— здесь PHP ищет свойphp.ini, подробности в следующем разделе.
Запуск не от root
Добавьте в блок [Service] строки User= и Group=: systemd передаст RuntimeDirectory этой учётной записи, и pid-файл вместе с Unix-сокетом внутри /run/rapira/ продолжат работать. А вот пути за её пределами, вроде /run/rapira.pid, лежат в каталоге, который принадлежит root, и открыть их не выйдет.
Два приложения на одной машине требуют двух конфигураций, двух юнитов и двух адресов прослушивания; для этого возьмите шаблонный юнит systemd (rapira@.service). Каждый экземпляр поднимает свой PHP и свой пул воркеров и не делит с другим экземпляром ничего, кроме машины.
Где лежит конфигурация
Соглашение такое: настройки самой Rapira — в /etc/rapira/rapira.toml, а рядом лежит php.ini, который PHP находит через PHPRC=/etc/rapira. Ни один из этих путей не зашит в бинарник. --config принимает любой путь, а PHPRC вообще не изобретение Rapira: в поиск ini-файлов Rapira не вмешивается, поэтому PHP сначала заглядывает в $PHPRC ровно так же, как под любым другим SAPI. Если ваш дистрибутив или ваша Ansible-роль использует другие пути, перенесите оба.
Rapira работает и вовсе без php.ini: её встроенные ini-умолчания уводят диагностику PHP в лог, а не в ответы клиенту, — об этом рассказывает Логирование. Свой файл в /etc/rapira пишите тогда, когда захотите настроить OPcache, ограничить память или задать часовой пояс; всё, что вы в нём зададите, окажется сильнее.
Относительный pool.entrypoint отсчитывается от каталога самого файла конфигурации, а не от рабочего каталога. При раскладке выше entrypoint = "index.php" означал бы /etc/rapira/index.php, где вашего приложения точно нет. В продакшене задавайте входному скрипту абсолютный путь, и вопрос отпадёт сам собой. supervisor.pidfile живёт по тому же правилу: оба пути из конфигурации отсчитываются от каталога с самим файлом конфигурации. А вот от рабочего каталога отсчитываются позиционный аргумент SCRIPT и любой относительный путь, который ваш PHP-код открывает уже во время работы, — сама же Rapira каталог никогда не меняет: без WorkingDirectory= systemd запустит службу в /, как раз поэтому в юните выше эта строка есть (в свой поиск ini-файлов PHP включает и ., так что заглянет он туда же). Каждый ключ со своим значением по умолчанию описан в разделе Конфигурация.
За обратным прокси
Rapira слушает открытый HTTP, и раздела про TLS в конфигурации нет. Терминируйте TLS на том прокси, который у вас и так работает — nginx, Caddy, HAProxy, облачный балансировщик, — а до Rapira пусть подключается через петлевой интерфейс или Unix-сокет. Привязаться к публичному интерфейсу вы можете, но и там этот слушатель отдаёт открытый HTTP.
[http]
listen = "127.0.0.1:8000"
# listen = "unix:/run/rapira/rapira.sock"Unix-сокет создаётся с правами 0666, поэтому любой локальный процесс, которому доступен каталог с сокетом, может подключиться и слать запросы вашему приложению. Настройки для этих прав у Rapira нет, так что доступ к сокету ограничивают только права самого каталога. Если для вас это важно, ограничьте сам каталог: в юните выше RuntimeDirectoryMode=0750 и Group=, в которую входит пользователь прокси, закроют /run/rapira для всех остальных.
Служебные поля должны доходить до Rapira в обычном написании через дефис — X-Forwarded-For, а не X_Forwarded_For. Написания с подчёркиванием и точкой схлопываются в тот же ключ $_SERVER, что и правильное, а значит, клиент мог бы переписать то, что только что выставил ваш прокси, — поэтому Rapira удаляет такие поля до того, как их увидит PHP. Само преобразование имён и настройку http.unsafe_field_names, которая им управляет, разбирает раздел HTTP.
Деплой без простоя
Выложите новый код и выполните:
sudo systemctl reload rapiraЭто SIGUSR2 мастер-процессу, а тот отвечает плавной перезагрузкой: пул обновляется по одному воркеру, начатые запросы доводятся до конца — и ничего не теряется, пока воркер укладывается в process_control_timeout_secs. Воркер, который не уложился, получает SIGTERM, затем SIGKILL, и его текущий запрос теряется (об этом ниже). Как именно свежий воркер перекрывается со старым, рассказывает Модель процессов.
Без systemd — из entrypoint контейнера или из скрипта деплоя — шлите сигнал мастер-процессу сами. Задайте supervisor.pidfile, и pid всегда будет под рукой. Учтите только, что за пределами systemd каталог /run/rapira никто не создаёт: сделайте его заранее или выберите существующий путь, иначе мастер-процесс просто откажется стартовать, не сумев записать файл.
[supervisor]
pidfile = "/run/rapira/rapira.pid"
process_control_timeout_secs = 30kill -USR2 "$(cat /run/rapira/rapira.pid)"Пишет в этот файл только мастер-процесс, воркеры к нему не прикасаются, и он же удаляет файл на каждом пути завершения, который контролирует. Поэтому оставшийся pid-файл означает, что мастер умер, не выполнив собственную остановку: SIGKILL, жёсткое падение или выключение машины.
process_control_timeout_secs — время, которое мастер-процесс даёт воркеру на завершение, прежде чем перейти к более жёстким мерам. Он же ограничивает каждый шаг плавной перезагрузки, так что один зависший воркер не застопорит её целиком: порядок ужесточения и полная таблица сигналов есть в разделе Модель процессов. Держите это значение с хорошим запасом ниже TimeoutStopSec в systemd, иначе таймаут systemd истечёт первым и он убьёт мастер-процесс прямо посреди этой последовательности.
Чего перезагрузка не делает
Мастер-процесс живёт с теми настройками, с которыми стартовал, и разделяемая память OPcache принадлежит тоже ему, поэтому она переживает любое поколение воркеров. Изменили rapira.toml — нужен systemctl restart rapira. И если у вас выставлено opcache.validate_timestamps = 0, после перезагрузки сервер продолжит отдавать старые опкоды: тут тоже нужен перезапуск.
Логи
Каждую запись лога Rapira пишет в stderr, ровно одной операцией записи, поэтому вывод мастер-процесса и воркеров никогда не перемешивается посреди строки. У юнита systemd stderr попадает в журнал вообще без настройки, так что выбрать остаётся только формат. В продакшене используйте JSON:
[log]
level = "info"
format = "json"Один объект на строку: timestamp в UTC по RFC 3339, а рядом level, message и target. Переводы строк внутри сообщения экранируются, так что запись всегда занимает ровно одну строку. Именно в таком виде логи ждут сборщики, и journald пропускает их через себя без изменений.
journalctl -u rapira -fЧтобы увезти логи с машины, направьте сборщик на журнал этого юнита или, если journald вам не нужен, запустите Rapira так, чтобы её stderr уходил прямо в агент. В обоих случаях запись уже структурирована, поэтому сборщику не придётся разбирать её регулярными выражениями. Про уровни для отдельных целей и про RUST_LOG, который на одну отладочную сессию заменяет весь фильтр целиком, рассказывает Логирование.
Обновление воркеров и таймауты запросов
В режиме воркера процесс остаётся резидентным, поэтому медленная утечка, незаметная под php-fpm, копится от запроса к запросу. От этого защищают две настройки:
[pool]
max_requests = 500
request_terminate_timeout_secs = 30max_requests завершает воркер после заданного числа запросов и форкает на его место свежий, добавляя к порогу небольшой разброс, чтобы весь пул не пошёл обновляться одновременно. Утечку это не лечит, зато не даёт ненайденной утечке превратиться в аварию. request_terminate_timeout_secs — потолок по реальному времени на один запрос: воркер, который в него не уложился, убивают и поднимают заново, так что один застрявший запрос не занимает воркер навсегда. По умолчанию выключены оба; включите их до выхода в продакшен.
Остальное про пул рассказывает Модель процессов: размеры пула в режимах static, dynamic и ondemand, нарастающие паузы перед повторным запуском и то, что делает мастер-процесс, когда воркер умирает.