Что такое systemd restart loop и почему он возникает
Под «systemd restart loop» обычно понимают ситуацию, когда сервис стартует, сразу падает и systemd тут же пытается поднять его снова. Если в unit-файле включён автоперезапуск (например, Restart=on-failure), это превращается в цикл: старт → ошибка → рестарт → снова ошибка.
Внешне кажется, что «сервис не поднимается», но в реальности он поднимается многократно, пока не упрётся в защиту от флаппинга: StartLimitBurst и StartLimitIntervalSec (или пока вы сами не остановите его).
Systemd почти всегда ведёт себя предсказуемо, если разделить проблему на две части:
- почему процесс завершается (первопричина падения);
- почему systemd решает перезапускать (настройки
Restart=и лимиты стартов).
Быстрая диагностика: systemctl status и journalctl -u
Начинайте со статуса: там часто уже видно и «почему упало», и «как systemd классифицирует завершение».
systemctl status myservice.service
Дальше — журнал юнита. Для unit troubleshooting самый полезный инструмент — journalctl -u:
journalctl -u myservice.service -b --no-pager
Если событий много, выведите только последние строки:
journalctl -u myservice.service -b -n 200 --no-pager
Обычно видно повторяющиеся блоки: «Started …», затем ошибка приложения или systemd, затем «Scheduled restart job …».
Как читать типичные сообщения о рестартах
Несколько характерных формулировок, которые важно различать:
- «Main process exited, code=exited, status=N» — процесс завершился сам, код возврата
N. - «code=killed, status=SIG…» — процесс убит сигналом (сегфолт, OOM killer, ручное завершение и т. п.).
- «Failed at step EXEC spawning …: No such file or directory» — systemd не смог выполнить
ExecStart(путь/права/формат). - «Start request repeated too quickly» — сработали лимиты
StartLimitBurst/StartLimitIntervalSec.
Если вам нужно быстро углубиться в диагностику unit’ов, полезно держать под рукой разбор очередей и воркеров: systemd для воркеров и процессов-работников.

Restart= в деталях: когда systemd перезапускает сервис
Параметр Restart= задаётся в секции [Service] и определяет, надо ли перезапускать unit после завершения процесса.
Практичные варианты:
Restart=no— не перезапускать (полезно на этапе отладки).Restart=on-failure— перезапускать при «ошибке»: ненулевой exit code, сигнал, таймаут, watchdog. Это самый частый и обычно правильный режим.Restart=always— перезапускать всегда, даже после выхода с кодом 0. Опасно, если процесс по дизайну завершается (миграции, одноразовые задачи).
Ещё один важный параметр — RestartSec. Он задаёт задержку перед перезапуском и спасает от «мясорубки» рестартов:
[Service]
Restart=on-failure
RestartSec=2s
Когда restart loop «создаём сами» неправильным Restart=
Классическая ошибка: поставить Restart=always на сервис, который отрабатывает и корректно завершается. Systemd увидит завершение и тут же поднимет его снова — получится бесконечный цикл «успешных» запусков.
Если unit должен «сделать дело и выйти», чаще нужен Type=oneshot и RemainAfterExit=yes, либо вообще не нужен Restart=.
StartLimitBurst и StartLimitIntervalSec: защита от флаппинга
Чтобы сервер не ушёл в бесконечные попытки старта, systemd применяет лимиты запуска. Они могут задаваться на уровне конкретного unit, а могут приходить по умолчанию из конфигурации systemd.
Два ключевых параметра:
StartLimitBurst— сколько запусков (включая рестарты) допускается;StartLimitIntervalSec— за какой интервал времени эти попытки считаются.
Если сервис превысил лимит, вы увидите «Start request repeated too quickly», и unit перейдёт в состояние failed, даже если Restart=on-failure включён.
Где настраиваются лимиты и почему вы их «не видите» в unit-файле
В актуальных systemd лимиты «старта» относятся к секции [Unit]. Пример явной настройки:
[Unit]
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=2s
Если вы не задавали эти параметры, проверьте эффективные значения:
systemctl show myservice.service -p StartLimitIntervalUSec -p StartLimitBurst
Обратите внимание: в выводе может быть микро-секундное представление (USec). Это нормально — systemd хранит многие значения в микросекундах.
Что делать, если сервис «упёрся в лимиты», а вам нужно продолжать отладку
Когда лимит сработал, повторный systemctl start может сразу возвращать ошибку из-за состояния failed и счётчиков. Обычно помогает такой порядок:
- Сбросить состояние failed и счётчики:
systemctl reset-failed myservice.service
- Остановить сервис и запустить снова после исправлений:
systemctl stop myservice.service
systemctl start myservice.service
Для быстрой отладки иногда временно увеличивают StartLimitBurst и StartLimitIntervalSec, но лучше не лечить симптом: важнее найти первопричину падения процесса.
Exit code 203: почему systemd падает «ещё до запуска»
Один из самых частых сценариев restart loop — ошибка запуска процесса со стороны systemd, а не вашего приложения. В логах это часто выглядит как exit code 203 (в терминах systemd — ошибка на шаге EXEC).
Exit code 203 обычно означает, что systemd не смог выполнить команду из
ExecStart: неверный путь к бинарнику, отсутствует файл, нет права на исполнение, неправильный пользователь/группа или окружение.
Чеклист для 203 (EXEC)
Проверяйте последовательно:
- Путь в
ExecStart— абсолютный ли он и существует ли файл. - Права — есть ли
xна бинарнике/скрипте. - Формат скрипта — если это shell-скрипт, есть ли корректный shebang (например,
#!/usr/bin/env bash) и не потерялись ли переводы строк. - Рабочая директория — если процесс зависит от файлов рядом, задайте
WorkingDirectory. - Пользователь — если указан
User=, может не быть доступа к файлам/каталогам.
Точную строку ошибки обычно видно так:
journalctl -u myservice.service -b --no-pager
Часто там будет прямая подсказка вроде «No such file or directory» или «Permission denied».
Правильный подход к unit troubleshooting: отделяем причину падения от реакции systemd
Практика, которая экономит время: временно выключить рестарты, чтобы не засорять журнал и не биться об лимиты, затем протестировать запуск.
1) Временно отключаем рестарты через drop-in override
Не редактируйте unit-файлы пакетов напрямую. Используйте override:
systemctl edit myservice.service
Добавьте:
[Service]
Restart=no
Примените изменения и перезапустите:
systemctl daemon-reload
systemctl restart myservice.service
Теперь сервис либо поднимется, либо упадёт один раз — и вы спокойно читаете причину в логах.
2) Тестируем команду ExecStart вручную
Возьмите команду из ExecStart и выполните её в shell (желательно от того же пользователя, что указан в User=). Так вы быстро поймёте, «не стартует вообще» или «стартует, но завершается».
Если unit использует переменные окружения (Environment=, EnvironmentFile=), учтите: в интерактивной оболочке их может не быть.
Типовые причины restart loop (и как исправлять)
Неправильный Type= и поведение процесса
Если вы запускаете форкающийся демон, а unit описан как Type=simple, systemd может решить, что основной процесс завершился, и начнёт рестартить. Для forking-демонов обычно нужен Type=forking и корректный PIDFile (если демон его пишет).
Для современных приложений, которые не форкаются и работают в foreground, чаще всего подходит Type=simple (по умолчанию) или Type=notify, если приложение умеет sd_notify.
Ошибки окружения и путей
Systemd запускает сервисы с «чистым» окружением. Если приложение в интерактивной сессии находит бинарники по PATH, а под systemd — нет, используйте абсолютные пути или задайте Environment=PATH=....
Слишком агрессивный Restart=always без задержки
Если вам действительно нужен Restart=always, добавляйте RestartSec. Иначе при мгновенном падении вы получите сотни событий за минуту, быструю блокировку по лимиту и «шум» в журнале.
Падение «на старте» из-за прав и пользователя
Распространённый кейс: сервис стартует как root в ручном запуске, но в unit задан User=app, а у пользователя нет доступа к WorkingDirectory, логам или сокетам. В результате процесс тут же падает, и включается restart loop.
Практические примеры настроек
Безопасный baseline для большинства демонов
Шаблон, который обычно «приятно» ведёт себя в проде:
[Unit]
Description=My Service
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp
Restart=on-failure
RestartSec=3s
[Install]
WantedBy=multi-user.target
Идея: перезапускать только при реальной ошибке, давать паузу, не позволять флаппингу бесконечно долбить систему.
Если вы отлаживаете такие сервисы на отдельной машине, удобнее делать это на VDS, чтобы спокойно перезапускать юниты, смотреть логи и не мешать продакшену.

Если сервис может «легально» завершаться с кодом != 0
Иногда приложение использует ненулевые коды «не как ошибку». Тогда Restart=on-failure будет считать это фейлом. В таких случаях полезно пересмотреть логику приложения или использовать SuccessExitStatus (только если вы уверены, что это корректно):
[Service]
ExecStart=/opt/myapp/bin/myapp
SuccessExitStatus=2 3
Restart=on-failure
RestartSec=2s
Как понять, что проблема решена (и не спрятана лимитами)
После правок сделайте стандартный цикл:
systemctl daemon-reload
systemctl restart myservice.service
Проверьте, что сервис стабильно в состоянии active:
systemctl is-active myservice.service
И убедитесь, что в журнале нет непрерывной череды рестартов:
journalctl -u myservice.service -b -n 100 --no-pager
Короткая памятка: что смотреть в первую минуту инцидента
systemctl status— видим последнее падение и причину (часто прямо пишется про 203/EXEC или «Start request repeated too quickly»).journalctl -u— ищем первичную ошибку, а не следствие (рестарты).- Сверяем
ExecStart(путь, права, shebang),User=,WorkingDirectory. - Проверяем, не слишком ли агрессивен
Restart=и есть лиRestartSec. - Если упёрлись в лимиты —
systemctl reset-failed, но только после исправлений.
Итог
Restart loop в systemd — это не «мистика», а предсказуемая комбинация: сервис падает + unit настроен на рестарт + лимиты запуска либо не настроены, либо уже сработали. В практике чаще всего виноваты ошибки ExecStart (включая exit code 203), права/пользователь, неверный Type= или Restart=always без задержек.
Если держать в голове схему «первопричина в журнале → реакция systemd в Restart/StartLimit», то unit troubleshooting становится быстрым: находите первую ошибку через journalctl -u, убираете причину, и только потом подбираете комфортные лимиты StartLimitBurst/StartLimitIntervalSec под нагрузку.


