php скрипт в systemd с отслеживанием вывода и деплоем

25.07.2021

Понадобилось засунуть php скрипт (с вечным циклом) в автозапуск через systemd: скрипт должен стартовать при старте системы и в случае своего падения должен возобновлять работу. При этом вывод скрипта очень хочется просмотреть в произвольный момент времени. Также нужен деплой с плавным перезапуском.

Демон systemd

Как-то раз, я уже писал про автозагрузку unicorn через systemd, освежим память и начнем.

Для начала создадим файл my-daemon.service в /etc/systemd/system/ и разместим там следующий конфиг:

•••
ini
[Unit] Description=my-daemon After=syslog.target network.target [Service] Type=simple WorkingDirectory=/home/byurrer/my-daemon/ User=root ExecStart=/usr/bin/php -f /home/byurrer/my-daemon/my-daemon.php & TimeoutSec=30 Restart=always RestartSec=10s [Install] WantedBy=multi-user.target

Запускаем:

  • после загрузки сети (After)
  • как обычное приложене без ветвления (Type=simple), но в самой команде запуска (ExecStart) используется & для перевода выполнения команды в фоновый режим. Здесь различия значений Type (forking vs simple)
  • от юзера root
  • с таймаутом ожидания остановки скрипта 30 секунд (TimeoutSec), если скрипт не завершится за это время то процесс будет закрыт принудительно
  • с постоянным перезапуском (Restart) через 10 секунд после падения (RestartSec)
Затем нужно активировать этот сервис:
•••
bash
systemctl enable my-daemon

Для отключения сервиса нужно:

•••
bash
systemctl disable my-daemon

Если в конфиг сервиса были внесены изменения (/etc/systemd/system/my-daemon.service) то необходимо перезагрузить конфиги демонов:

•••
bash
systemctl daemon-reload

Просмотр текущего статуса:

•••
bash
systemctl status my-daemon

Вместо status можно использовать другие команды systemctl:

  • start - старт
  • stop - остановка
  • restart - перезапуск, например при изменении кода скрипта

Просмотр вывода

Для того чтобы направить вывод демона в терминал, можно использовать reptyr:

•••
bash
reptyr PID

PID можно узнать через запрос статуса демона (в данном выводу pid=5965):

•••
bash
systemctl status my-daemon ● my-daemon - my-daemon Loaded: loaded (/etc/systemd/system/my-daemon.service; enabled; vendor preset: enabled) Active: active (running) since Mon 2021-07-19 20:47:57 MSK; 145ms ago Main PID: 5965 (php) Tasks: 1 (limit: 9280) Memory: 5.7M CGroup: /system.slice/my-daemon.service └─5965 /usr/bin/php -f /home/byurrer/my-daemon/my-daemon.php &

После направления вывода сервиса в текущий терминал, скрыть/перенаправить вывод процесса из текущего терминала не представляется возможным, закрытие терминала приведет к закрытию процесса. Но эту проблему можно решить при помощи screen

Деплой без веба

Если есть доступ к серверу по http, то для деплоя можно использовать веб-хуки при push в удаленный репозиторий.

Но доступа по http нет. Поэтому напишем скрипт деплоя и повесим его на крон :)

Скрипт деплоя

Сначала нам нужно узнать, на что указывает HEAD в удаленном репозитории:

•••
bash
> git ls-remote -h origin 5da44ad4a189dc6a70d4c82b847abd9edfd461cc refs/heads/master

Теперь нам нужно сравнить с HEAD из локального репозитория (уже рассматривали в этой статье):

•••
bash
> git rev-parse HEAD edd43d00722a835a6ec24df14c38e103894d4128

Теперь мы имеем указатели на HEAD в удаленном репозитории и в локальном, если они одинаковые, значит наш локальный репозиторий не нуждается в обновлении, иначе обновляем:

•••
bash
git pull

Но есть проблема, первая и третья команды не могут успешно выполниться если удаленный репозиторий приватный. Для этого придется явно указывать в URL авторизационные данные.

При наличии self-hosted gitlab можно использовать токены доступа, а если есть аккаунт на gitlab.com можно использовать данные от своего аккаунта или создать дополнительный аккаунт для деплоя.

В итоге скрипт для деплоя будет выглядеть следующим образом (разместить надо в корне локального репозитория):

•••
php
$sDir = dirname(__FILE__); $sResultLsRemote = shell_exec("cd $sDir; git ls-remote -h https://login:password@gitlab.com/user/repo.git"); if(!preg_match("/^(.*?)\s/", $sResultLsRemote, $aMatch)) exit("unresolved response\n"); $sLastCommitRemote = trim($aMatch[1]); $sLastCommitLocal = trim(shell_exec("cd $sDir; git rev-parse HEAD")); if($sLastCommitRemote == $sLastCommitLocal) exit("no change\n"); $sResultPull = shell_exec("cd $sDir; git pull https://login:password@gitlab.com/user/repo.git");

Для перезапуска демона добавим конец такое (скрипт должен запускаться от имени root):

•••
php
shell_exec("systemctl restart my-daemon");

Cron

Совсем недавно мы рассматривали установку certbot в cron.

Делаем все аналогично, от имени root сначала создаем файл /erc/cron.d/my-daemon-deploy.php (*/10 - каждые 10 минут):

•••
bash
*/10 * * * * root php7.3 -f /home/byurrer/my-daemon/.deploy.php

Затем рестарт демона cron:

•••
bash
systemctl restart cron

Плавный перезапуск демонов

Представим ситуацию что php скрипт (который демонизируется) выполняется в бесконечном цикле и каждая итерация цикла работает с данными, которые нельзя потерять, например так:

•••
php
$redis->sPop($key, 100); //...

Тогда нам нужно плавное завершение, которое позволит завершить итерацию бесконечного цикла и прервать работу скрипта без потери данных. Для этого в php можно использовать установку обработчика сигнала SIGTERM при помощи функции pcntl_signal.

В моем случае все свелось к минимуму:

•••
php
declare(ticks = 1); // нужно ли завершать исполнение $g_needTerminate = false; // обработчик сигналов function SigHandler($signo) { global $g_needTerminate; switch ($signo) { case SIGTERM: $g_needTerminate = true; break; default: break; } } // установка обработчика сигнала SIGTERM pcntl_signal(SIGTERM, "SigHandler"); // бесконечный цикл, каждая итерация которого зависит от $g_needTerminate while(!$g_needTerminate) { //... }

Итог

В итоге получается php демон в systemd, который:
  • запускается при старте системы
  • перезапускается в случае падения
  • при деплое плавно завершает свою работу и перезагружается с обновлением