SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Почему systemd не перезапускает службу при сбое

Директива Restart отслеживает только основной процесс юнита. Если дочерний процесс завершается с ошибкой, systemd считает службу активной. Узнайте, как работают лимиты и Type.

Краткий ответ: политики перезапуска systemd отслеживают один процесс

Политики перезапуска systemd отслеживают только один процесс на юнит: основной процесс. Restart= считывает код завершения только этого процесса и ничего больше. В контрольной группе юнита может находиться двадцать процессов, один из них может завершиться, но статус юнита останется active (running), так как основной процесс всё ещё работает. С точки зрения systemd сбоя не произошло, поэтому перезапуск не выполняется.

systemd «знает» о существовании других процессов. Он завершает их при остановке юнита, учитывает их потребление памяти в лимитах юнита, применяет к ним квоты CPU и отображает их в systemctl status. Однако он никогда не считывает их коды завершения. Логика перезапуска и cgroup — это разные механизмы, и большая часть этого руководства посвящена разрыву между ними.

Что содержит cgroup и как работает логика перезапуска

Cgroup (control group) — это объект ядра, которому принадлежит набор процессов. Каждый юнит службы получает свою cgroup, названную в честь этого юнита. Процесс не может покинуть её. Дочерние процессы наследуют cgroup родителя, а процесс без привилегий не может переместить себя в другое место. Именно поэтому systemd способен корректно завершить работу демона, который делает двойной fork, чего старые скрипты инициализации никогда не могли делать надёжно.

Рассмотрите оба факта вместе:

systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.service

systemd-cgls перечисляет все процессы в юните. MainPID — это единственное число, которое считывает политика перезапуска. Если эти данные расходятся с вашим представлением о работе системы, значит, ошибка кроется в этом расхождении. MainPID=0 хуже, чем неверный PID: это означает, что systemd вообще ничего не отслеживает, поэтому никакое значение Restart= никогда не сработает.

Существует одно реальное исключение из правила главного процесса. Если OOM-killer (out-of-memory killer) ядра убивает любой процесс внутри cgroup юнита, systemd видит это, так как отслеживает файл memory.events этой cgroup. OOMPolicy= определяет, что произойдёт дальше, и по умолчанию установлено значение stop: весь юнит останавливается, результат записывается как oom-kill, и это считается сбоем, поэтому срабатывает Restart=on-failure. Журнал сообщает об этом прямо.

myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Failed with result 'oom-kill'.

Таким образом, дочерний процесс, убитый из-за нехватки памяти, приводит к остановке юнита, в то время как тот же процесс, завершившийся с ошибкой сегментации (segmentation fault), — нет. Если вы устанавливаете лимиты памяти для юнита, прочитайте как MemoryMax и CPUQuota применяются к cgroup юнита перед настройкой политики перезапуска, так как именно здесь эти две функции пересекаются.

Как Type= определяет основной процесс

Type= в секции [Service] определяет не только порядок запуска. Это правило решает, какой PID (идентификатор процесса) станет MainPID, что равносильно определению того, что именно Restart= сможет отслеживать.

  • Type=simple — значение по умолчанию. Процесс, который systemd запускает через fork из ExecStart=, считается основным. systemd помечает юнит как запущенный немедленно, не дожидаясь подтверждения того, что exec вообще сработал. Опечатка в пути к бинарному файлу приведет к тому, что задача запуска завершится успешно, а затем Main process exited, code=exited, status=203/EXEC через мгновение.
  • Type=exec ведет себя как simple, но задача запуска ожидает, пока exec не завершится успешно. Это превращает опечатку, описанную выше, в честный сбой запуска. Требуется systemd 240 или новее, что есть в любом поддерживаемом дистрибутиве. Отдавайте предпочтение этому значению перед simple.
  • Type=forking ожидает, что процесс из ExecStart= создаст фоновый демон и завершится. systemd ждет завершения родительского процесса, а затем ищет настоящий демон. Укажите PIDFile=. Без него GuessMainPID= (включен по умолчанию) работает только тогда, когда в cgroup остается ровно один процесс. Оставьте два процесса, и MainPID останется в состоянии 0.
  • Type=notify означает, что сервис вызывает sd_notify(3) и отправляет READY=1, когда готов принимать трафик. Он также может отправить MAINPID=, чтобы передать systemd другой процесс для отслеживания. NotifyAccess= по умолчанию имеет значение main, поэтому уведомление, отправленное дочерним процессом, игнорируется, а в журнале указывается PID, от которого оно поступило.
  • Type=oneshot не предполагает наличия постоянного основного процесса. Юнит переходит в состояние неактивности сразу после завершения ExecStart=, если не задан параметр RemainAfterExit=yes. Restart=always и Restart=on-success здесь запрещены, при попытке их использования выводится сообщение Service has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.. Остальные значения, включая on-failure, принимаются.

Стоит запомнить две ошибки Type=forking, так как обе приводят к тому, что юнит выглядит нерабочим без видимых причин:

myapp.service: Can't open PID file /run/myapp.pid (yet?) after start: No such file or directory
myapp.service: New main PID 4711 does not belong to service, and PID file is not owned by root. Refusing.

Первая означает, что демон записывает PID-файл в другое место или записывает его позже, чем systemd пытается его прочитать. Вторая означает, что PID-файл указывает на процесс вне cgroup юнита, который systemd отказывается принимать, так как в противном случае запись в PID-файл стала бы способом заставить systemd отправить сигналы любому процессу в системе.

Почему скрипт-обертка скрывает завершение дочерних процессов

Вот структура, которая приводит к вопросу в заголовке.

#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait

Юнит имеет тип Type=simple, поэтому основным процессом является оболочка (shell). Команда wait без аргументов завершается только после выхода всех дочерних процессов. Если завершить рабочий процесс (worker), оболочка продолжит ожидать веб-процесс, поэтому оболочка не завершится, MainPID не завершится, и Restart= никогда не будет вызван. В cgroup теперь на один процесс меньше, systemctl status выводит сокращенное дерево, а юнит всё еще находится в состоянии active (running). systemd не отслеживает изменения в этом дереве.

Вторая версия той же ошибки проявляется менее заметно:

ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'

Основным процессом является оболочка, а не myapp. При выполнении systemctl stop systemd отправляет SIGTERM основному процессу, а оболочка, ожидающая дочерний процесс в foreground, не передает этот сигнал дальше. Остановка занимает полные TimeoutStopSec (по умолчанию 90 секунд) и завершается следующим образом:

myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 4711 (myapp) with signal SIGKILL.

Решение заключается в использовании exec. Напишите exec /usr/local/bin/myapp, и оболочка будет заменена программой, поэтому MainPID станет самой программой, и сигналы будут доходить до неё. Еще лучше — отказаться от оболочки и использовать Environment= или EnvironmentFile= в юните. Обратите внимание, что эта ошибка маскируется, когда строка -c содержит только одну команду, так как bash и dash оптимизируют этот случай в прямой вызов exec. Добавьте вторую команду в строку, и оболочка останется активной перед вашей программой.

Воспроизведение на тестовом VPS за две минуты

Сохраните приведенную выше обертку как /usr/local/bin/two-children.sh, сделайте её исполняемой с помощью chmod +x и замените пути к программам на sleep 3600. Укажите на неё в юните с помощью Type=simple и Restart=on-failure, затем выполните systemctl daemon-reload и запустите его. Выполните systemd-cgls --unit two-children.service и обратите внимание на три PID: оболочку и два её дочерних процесса. Завершите один дочерний процесс командой sudo kill <pid>. Снова проверьте юнит. Дерево стало короче на один процесс, состояние всё еще active (running), а в журнале нет новых записей. Теперь выполните sudo kill -9 <shell pid>. Юнит переходит в состояние сбоя, выживший дочерний процесс завершается, так как KillMode=control-group является значением по умолчанию, а в журнале отображается Scheduled restart job, restart counter is at 1.

Полный список значений Restart= и причины предпочесть on-failure вместо always

Restart= принимает одно из семи значений, и различие между ними заключается в определении «чистого» завершения. systemd считает чистым завершением код выхода 0, любой код, указанный в SuccessExitStatus=, а также сигналы SIGHUP, SIGINT, SIGTERM и SIGPIPE. Всё остальное, включая SIGKILL и SIGSEGV, считается нечистым завершением.

  • no — значение по умолчанию. Юнит никогда не перезапускается самостоятельно, поэтому юнит без строки Restart= завершается при первом же сбое и остаётся в неактивном состоянии.
  • on-success перезапускает юнит только после чистого завершения.
  • on-failure перезапускает юнит при ненулевом коде выхода, нечистом сигнале, превышении тайм-аута запуска или остановки, либо при срабатывании watchdog.
  • on-abnormal перезапускает юнит при нечистом сигнале, превышении тайм-аута или срабатывании watchdog, но никогда при обычном ненулевом коде выхода.
  • on-abort перезапускает юнит только при нечистом сигнале, что означает аварийное завершение (crash).
  • on-watchdog перезапускает юнит только по истечении WatchdogSec=.
  • always перезапускает юнит во всех перечисленных выше случаях, включая чистое завершение с кодом 0.

on-failure — оптимальный выбор по умолчанию для постоянно работающего демона. Он восстанавливает сервис после сбоя и не трогает его при намеренном exit 0. always подходит для программ, которые завершаются чисто по независящим от них причинам, например, для клиента туннеля, возвращающего 0 при разрыве соединения на удалённой стороне. Недостаток always в том, что он скрывает ошибки: сервис, который запускается, читает повреждённый файл конфигурации, записывает ошибку в лог и завершается с кодом 0, будет перезапускаться бесконечно, и единственным признаком проблемы станет растущий счётчик перезапусков.

SuccessExitStatus= меняет границу между чистым и нечистым завершением. Borg возвращает 1 при предупреждениях и 2 при ошибках, поэтому юнит резервного копирования без SuccessExitStatus=1 будет помечаться как упавший каждый раз, когда он пропускает один нечитаемый файл. RestartPreventExitStatus= перечисляет коды, которые блокируют перезапуск даже при always; это правильный способ для программы сообщить, что её не нужно перезапускать. RestartForceExitStatus= делает обратное. Задачу резервного копирования лучше поместить в юнит Type=oneshot, запускаемый по таймеру, а не в цикл перезапуска; пара сервиса и таймера для запуска задачи по расписанию — это шаблон, который следует использовать в данном случае.

Предупреждение о тестировании. Завершение сервиса командой kill <pid> отправляет сигнал SIGTERM, который входит в список чистых завершений, поэтому Restart=on-failure корректно не предпринимает никаких действий, и вы можете ошибочно решить, что конфигурация не работает. Используйте kill -9 <pid> или systemctl kill -s SIGKILL myapp.service. Также помните, что ни одно значение Restart= не срабатывает после systemctl stop или когда юнит был остановлен из-за того, что исчезла зависимость BindsTo= или PartOf=. Задача остановки не является сбоем.

RestartSec и значение по умолчанию в 100 миллисекунд

RestartSec= — это пауза между остановкой юнита и его повторным запуском через systemd; по умолчанию она составляет 100 миллисекунд. Проверьте, какое значение фактически загружено для вашего юнита:

systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.service

Если в юните не задан параметр, выводится RestartUSec=100ms. Это значение по умолчанию подходит для сервиса, который аварийно завершается один раз и сразу восстанавливается. Оно не подходит для сервиса, который не может запуститься вовсе: пять попыток перезапуска произойдут менее чем за полсекунды, что гарантированно приведет к срабатыванию ограничения частоты, описанного далее. Для любого сервиса, ожидающего базу данных, точку монтирования или сетевой маршрут, установите RestartSec=5s или больше.

На август 2026 года в systemd версии 254 и новее также доступны RestartSteps= и RestartMaxDelaySec=. Они позволяют увеличивать задержку от RestartSec= до заданного предела по мере выполнения попыток. Ubuntu 24.04 поставляется с systemd 255 и поддерживает эти параметры. В Debian 12 используется systemd 252, где они отсутствуют. Увеличение задержки — оптимальное решение, если зависимость может быть недоступна в течение длительного времени.

Что на самом деле означает ошибка "start request repeated too quickly"

Это состояние, которое пользователи часто принимают за произвольный отказ systemd. На самом деле это счетчик. Правило таково: если юнит запускается более StartLimitBurst= раз в течение StartLimitIntervalSec=, systemd отказывается запускать его снова и переводит в состояние failed. По умолчанию установлено 5 запусков за 10 секунд.

Журнал показывает следующую последовательность:

myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
Failed to start myapp.service - My application.

а systemctl start дает ответ с уже готовым решением:

Job for myapp.service failed because start of the service was attempted too often. See "systemctl status myapp.service" and "journalctl -xeu myapp.service" for details. To force a start use "systemctl reset-failed myapp.service" followed by "systemctl start myapp.service" again.

systemctl reset-failed myapp.service сбрасывает счетчик и состояние ошибки. Никакие другие действия этого не делают, поэтому обычный systemctl start будет постоянно приводить к отказу, пока вы не выполните эту команду. Ручные запуски также учитываются в лимите, поэтому несколько нетерпеливых попыток systemctl restart во время редактирования конфигурационного файла могут вызвать блокировку даже без реального сбоя приложения.

То, что вводит людей в заблуждение: start-limit-hit никогда не сообщает, почему сервис завершался с ошибкой. Сообщение лишь указывает на то, что сбои происходили часто и многократно. Истинная причина находится в строках журнала, расположенных выше.

Обе настройки относятся к секции [Unit]. Вы можете встретить примеры, где они помещены в [Service], что допускалось в старых версиях systemd, и именно отсюда начинается путаница. Указывайте их в [Unit], а затем проверяйте, что именно загрузил systemd с помощью systemctl show, так как учитывается только загруженное значение.

[Unit]
Description=My application
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=exec
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10s

Эта конфигурация дает юниту пять попыток в течение пятиминутного окна, прежде чем система сдастся. StartLimitIntervalSec=0 полностью отключает лимит, но вы должны понимать последствия: сервис, который не может запуститься, будет пытаться сделать это бесконечно, записывая ошибки в журнал при каждой попытке. Глобальные настройки для всей системы находятся в /etc/systemd/system.conf под именами DefaultStartLimitIntervalSec= и DefaultStartLimitBurst=.

Один соседний параметр заслуживает предостережения. StartLimitAction= определяет, что произойдет при достижении лимита, и принимает значения, включая reboot, reboot-force и poweroff. По умолчанию установлено none, что переводит юнит в состояние ошибки и не затрагивает остальную систему. На удаленном VPS значение poweroff означает, что сервер выключится и останется в таком состоянии, пока вы не откроете консоль провайдера.

Исправление первое: один процесс на юнит

Это решение подходит почти в каждом случае. Если должны работать две программы, создайте два юнита. У каждого из них будет свой основной процесс, свой статус завершения и своя политика перезапуска. Вы также получите раздельные логи, отдельные лимиты ресурсов и независимые счетчики перезапусков — именно то, что нужно в три часа ночи.

Описывайте взаимосвязи между юнитами в файлах юнитов, а не в shell-скриптах.

  • After= задает только порядок запуска. Он ничего не говорит о сбоях.
  • Requires= запускает другой юнит вместе с этим и останавливает текущий, если другой был остановлен явно.
  • BindsTo= — это Requires= плюс случай, который вас интересует: текущий юнит останавливается, если другой прекращает работу по любой причине, включая аварийное завершение. Используйте его в паре с After=, иначе порядок запуска будет неопределенным.
  • PartOf= распространяет остановку и перезапуск вниз, поэтому systemctl restart myapp.target затрагивает каждый юнит, который PartOf= его.
  • Upholds= (systemd 249 и новее, то есть Ubuntu 22.04 и позже) поддерживает работу указанного юнита: если он останавливается, systemd запускает его снова. На него распространяется то же ограничение частоты запуска, что и на все остальные.

Пример воркера, который никогда не должен работать без своего API-сервера, и который systemd поддерживает активным, пока API запущен:

# /etc/systemd/system/myapp-api.service
[Unit]
Description=myapp API server
Wants=network-online.target
After=network-online.target
Upholds=myapp-worker.service

[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp serve
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/myapp-worker.service
[Unit]
Description=myapp background worker
BindsTo=myapp-api.service
After=myapp-api.service
StartLimitIntervalSec=120
StartLimitBurst=5

[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp worker
Restart=on-failure
RestartSec=5s

У воркера нет секции [Install], и его не нужно включать вручную. Юнит API подтягивает его с помощью Upholds=, поэтому systemctl enable --now myapp-api.service — единственная команда, которую вам нужно выполнить. Перезагрузите конфигурацию и проверьте, как systemd обработал эту пару:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/myapp-worker.service
systemctl list-dependencies myapp-api.service

systemd-analyze verify не выводит ничего, если файл корректен. Любой вывод означает проблему: обычно это ключ, который systemd не распознает в указанной секции, или зависимость от юнита, которого не существует.

Исправление второе: Type=notify, чтобы systemd знал больше, чем просто PID

Если программа поддерживает протокол уведомлений systemd, используйте его. С помощью Type=notify сервис сообщает systemd о своей готовности. Это делает порядок запуска реальным, а не основанным на ожиданиях. Кроме того, можно отправить MAINPID=, чтобы указать systemd на основной процесс, а не на скрипт-запускатель.

WatchdogSec= — это та часть, ради которой стоит приложить усилия. Установите этот параметр, и сервис будет обязан отправлять WATCHDOG=1 через sd_notify(3) как минимум с такой частотой. Когда сообщения перестают поступать, systemd завершает сервис сигналом SIGABRT и помечает его как failed, после чего Restart=on-failure или Restart=on-watchdog возвращают его в работу. Это единственный встроенный способ перезапустить процесс, который «жив», но завис; ни одна политика по кодам завершения не способна это отследить.

[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/bin/myapp serve
WatchdogSec=30s
Restart=on-failure
RestartSec=5s

Срабатывание watchdog отображается в журнале как myapp.service: Watchdog timeout (limit 30s)!, за которым следует завершение процесса. Если юнит зависает в состоянии activating (start) до истечения TimeoutStartSec, значит, READY=1 так и не поступило. Либо программа не поддерживает протокол, либо NotifyAccess=main отклоняет уведомление, пришедшее от дочернего процесса (в этом случае журнал покажет оба PID).

Для ПО, которое предоставляет HTTP-эндпоинт для проверки состояния (health endpoint), но не имеет поддержки sd_notify, есть два честных варианта: небольшой таймер, который опрашивает эндпоинт и вызывает systemctl restart, либо использование возможностей среды выполнения контейнеров, для чего и существуют Compose healthchecks and their restart behaviour.

Исправление три: супервизор внутри юнита, только если нет другого выбора

Некоторое ПО поставляется как набор процессов, управляемых лаунчером, который невозможно разделить. В этом случае вы запускаете супервизор внутри юнита и принимаете последствия: systemd следит за супервизором, супервизор следит за всем остальным, а политика перезапуска теперь описана в двух разных файлах.

Распространенный пример — среда выполнения контейнеров. Юнит docker compose или podman в точности соответствует этому шаблону: политика перезапуска для каждого контейнера задается в файле Compose, а systemd-юнит лишь поддерживает работу самой среды выполнения. Если ваш случай именно такой, юнит, запускающий стек Compose при загрузке демонстрирует рабочий вариант, включая объяснение, почему Type=oneshot с RemainAfterExit=yes обычно является здесь правильным решением.

Cgroup по-прежнему работает в вашу пользу. Все, что запускает супервизор, остается внутри cgroup юнита, поэтому MemoryMax=, CPUQuota= и очистка при остановке по-прежнему охватывают всё дерево процессов. Делегируется только решение о перезапуске.

Какой бы супервизор вы ни выбрали, не устанавливайте Restart=always для внешнего юнита одновременно с агрессивной политикой перезапуска внутри него без предварительного анализа. Два уровня логики перезапуска, каждый со своей задержкой, приведут к тому, что сервис будет циклически перезапускаться в течение нескольких минут, а журнал не даст объяснения причин такого поведения.

ExitType=cgroup не означает «перезапускать при завершении любого процесса»

ExitType= (systemd 250 и новее, присутствует в Ubuntu 24.04 и Debian 12) — это настройка, которую находят при поиске решения данной проблемы, но она работает не так, как можно предположить из названия. Значение по умолчанию, ExitType=main, означает, что сервис считается остановленным, когда завершается основной процесс. ExitType=cgroup означает, что сервис считается запущенным до тех пор, пока не завершится последний процесс в cgroup.

Таким образом, ExitType=cgroup делает юнит менее чувствительным к завершению отдельного процесса, а не более. Это подходящая настройка для программы, которая порождает (fork) рабочий процесс и завершает родительский, не записывая PID-файл, из-за чего Type=forking не может найти демон. Это неверная настройка для описанной здесь ошибки.

Не существует значения Restart=, которое означало бы «перезапускать юнит при завершении любого процесса в cgroup». Если вам нужно такое поведение, необходимо использовать по одному процессу на юнит. Если вы не можете разделить программу, но управляете скриптом-оберткой, ближайшим решением будет wait -n, который завершается сразу после выхода первого дочернего процесса:

#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1

Теперь завершение любого дочернего процесса приводит к остановке обертки с ненулевым кодом выхода, поэтому Restart=on-failure срабатывает. Это компромисс, а не полноценное исправление. Вы по-прежнему получаете один счетчик перезапусков на две программы, один поток логов и отсутствие возможности перезапустить только сбойную часть.

Как проверить, что произошло на самом деле

Четыре команды в указанном порядке.

systemctl status myapp.service
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Result -p ExecMainStatus myapp.service
journalctl -u myapp.service -b -o short-precise

systemctl status выводит состояние, основной PID и дерево cgroup на одном экране. У исправного юнита статус Active: active (running), а строка Main PID: содержит процесс, который вы ожидаете увидеть. Если в дереве внизу перечислены процессы, которые вы не узнаёте, или отсутствует нужный вам процесс, причина проблемы уже найдена.

systemd-cgls --unit выводит то же дерево без сокращений, что становится важным, когда юнит содержит более нескольких процессов.

systemctl show предоставляет данные в машиночитаемом формате. NRestarts= — это счетчик перезапусков, и это самый быстрый способ отличить сервис, который перезапускался сорок раз, от того, который работает с момента загрузки. Result= содержит причину последнего сбоя: exit-code, signal, timeout, oom-kill, watchdog или start-limit-hit. ExecMainStatus= — это необработанный код завершения последнего основного процесса.

Журнал содержит последовательность событий. Вот три строки, которые нужно искать:

myapp.service: Main process exited, code=exited, status=1/FAILURE
myapp.service: Failed with result 'exit-code'.
myapp.service: Scheduled restart job, restart counter is at 1.

code=exited, status=N означает, что программа сама решила вернуть код N, поэтому ошибка кроется в самой программе или её конфигурации. code=killed, signal=SEGV означает, что произошел аварийный сбой. code=killed, signal=TERM обычно означает, что кто-то другой попросил процесс остановиться; это не является сбоем и не вызовет Restart=on-failure. code=dumped означает, что был создан core dump, который coredumpctl list покажет вам, если установлен systemd-coredump.

При работе с несколькими машинами NRestarts — это показатель, который стоит собирать по расписанию. Юнит, счетчик которого растет каждый день, дает сбой ежедневно, независимо от того, заметил это кто-то или нет. Как только у вас становится больше двух или трех серверов, единый способ выполнения команды на всех серверах превращает догадки в отчет.

FAQ

Почему systemctl сообщает, что сервис активен, хотя процесс завершился?

systemd отслеживает один процесс на юнит сервиса — основной процесс, и Restart= считывает только код завершения этого процесса. Все остальные процессы, запущенные юнитом, находятся в той же cgroup, и systemd завершит их при остановке юнита, но не отслеживает их завершение. Выполните systemctl show -p MainPID myapp.service и сравните результат с systemd-cgls --unit myapp.service. Если завершившийся процесс присутствует в дереве, но не является MainPID, значит, systemd отработал штатно. Решение заключается в использовании одного процесса на юнит с настройкой связей BindsTo= и Upholds= между юнитами.

Что означает ошибка "start request repeated too quickly"?

Это означает, что юнит был запущен более StartLimitBurst= раз за время StartLimitIntervalSec= (по умолчанию — 5 запусков за 10 секунд), поэтому systemd прекратил попытки. Это ограничение частоты, оно не указывает на причину сбоя сервиса, поэтому изучите строки журнала выше. Сбросьте состояние командой systemctl reset-failed myapp.service, затем устраните основную причину сбоя. Если сервис ожидает медленного запуска зависимостей, увеличьте RestartSec=, так как стандартный интервал в 100 миллисекунд исчерпывает все пять попыток менее чем за секунду.

Что использовать: Restart=always или Restart=on-failure?

Используйте on-failure почти для всех случаев. Этот параметр перезапускает сервис при аварийном завершении, ненулевом коде выхода, таймауте или срабатывании watchdog, при этом не затрагивая намеренный exit 0. Используйте always только в тех случаях, когда программа завершается корректно по причинам, не зависящим от неё, например, клиент, возвращающий 0 при разрыве соединения с узлом. Недостаток always в том, что сервис, который считывает поврежденный конфиг, записывает одну ошибку и завершается с кодом 0, будет перезапускаться бесконечно, а единственным видимым симптомом станет рост NRestarts в systemctl show.

Почему завершение процесса вручную не вызывает перезапуск?

Потому что systemd считает SIGHUP, SIGINT, SIGTERM и SIGPIPE корректным завершением, а обычный kill <pid> отправляет SIGTERM. Согласно Restart=on-failure, корректное завершение не является сбоем, поэтому перезапуск не происходит, и конфигурация кажется нерабочей, хотя это не так. Проверяйте работу с помощью kill -9 <pid> или systemctl kill -s SIGKILL myapp.service — это некорректное завершение, которое активирует политику перезапуска. Это же правило объясняет, почему systemctl stop не конфликтует с вашей политикой перезапуска.

Где должны быть указаны StartLimitIntervalSec и StartLimitBurst?

В секции [Unit]. В старых руководствах и версиях systemd они размещались в [Service], поэтому примеры из разных источников противоречат друг другу. Не пытайтесь угадать, какой вариант поддерживает ваша версия. После systemctl daemon-reload проверьте, что именно загрузил systemd, с помощью systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service, и считайте эти значения верными. systemd-analyze verify /etc/systemd/system/myapp.service выявляет ключи, которые systemd не распознает, и не выводит ничего, если файл конфигурации корректен.

#systemd#restart#service-unit#cgroups#reliability