MAATRIX / Блог / Сколько раз можно перезапускать сервис в минуту: лимиты рестарта, которые душат

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

MAATRIX

Вы включили Restart=on-failure, сервис упал, systemd честно поднял его снова — и через несколько секунд снова, и снова. А потом сервис просто перестал запускаться, хотя вы ничего не трогали. Это не мистика и не баг systemd: сработала защита от петли рестартов, которая молча переводит юнит в failed после нескольких быстрых падений подряд. Разберём, как устроен этот лимит, как его настроить под конкретный сервис и, главное, как за пару команд отличить «кончился лимит рестартов» от «сервис реально сломан».

Как работает защита от петли рестартов

У systemd есть встроенный механизм, который не даёт сервису бесконечно дёргаться в цикле «упал → перезапустился → упал». Он завязан на двух параметрах юнита, которые задаются в секции [Unit]:

  • StartLimitIntervalSec — длина скользящего окна времени, внутри которого считаются попытки запуска.
  • StartLimitBurst — сколько попыток запуска разрешено внутри этого окна, прежде чем systemd сдастся.

В большинстве актуальных дистрибутивов дефолт для этих параметров — 5 попыток запуска за 10 секунд (StartLimitBurst=5, StartLimitIntervalSec=10s), если юнит не переопределяет их явно. Проверить фактические значения для конкретного сервиса можно так:

systemctl show nginx.service -p StartLimitIntervalSec -p StartLimitBurst

Важно понимать, что считается: каждый *запуск* юнита, а не только падения. То есть если вы вручную перезапускаете сервис несколько раз подряд во время отладки, вы точно так же расходуете лимит, как если бы он падал сам.

Как только счётчик запусков внутри окна превышает StartLimitBurst, systemd останавливается и переводит юнит в состояние failed с результатом start-limit-hit. Дальше он не делает вообще ничего — ни новых попыток запуска, ни Restart=, ни уведомлений, кроме записи в журнал. С этого момента сервис недоступен не потому, что не может подняться, а потому что systemd ему просто не даёт попытаться.

Обратите внимание на грабли с секциями: StartLimitIntervalSec и StartLimitBurst относятся к секции [Unit], а не [Service]. В старых unit-файлах и мануалах можно встретить устаревшие имена StartLimitInterval= и StartLimitBurst= в [Service] — они до сих пор поддерживаются для обратной совместимости, но лучше использовать актуальные имена в [Unit], иначе легко запутаться, почему override не действует.

Что вы увидите, когда сервис уйдёт в failed

Диагностика начинается с того, чтобы вообще понять — это лимит рестартов или сервис действительно не поднимается. Признаки первого случая довольно характерны.

systemctl status покажет что-то вроде:

● myapp.service - My Application
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
     Active: failed (Result: start-limit-hit) since ...

Ключевая фраза здесь — Result: start-limit-hit. Это не код возврата вашего приложения и не сигнал, которым его убило — это результат, который systemd проставляет сам себе, когда исчерпан лимит попыток. Если вы видите start-limit-hit, искать баг в приложении по этому результату бессмысленно: он ничего не говорит о том, что было не так с сервисом, только о том, что systemd прекратил попытки.

В журнале это выглядит так:

journalctl -u myapp.service --since "-10min" --no-pager

Вы увидите частую последовательность Scheduled restart job, restart counter is at N, а в конце — строки вида myapp.service: Start request repeated too quickly и myapp.service: Failed with result 'start-limit-hit'. Реальная же причина первого падения (то, из-за чего сервис вообще начал крутиться в рестартах) находится *выше* этих строк в журнале — обычно это последний осмысленный вывод приложения перед тем, как оно упало в первый раз.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Restart= и как разные политики попадают под лимит

Лимит рестартов применяется независимо от того, какая политика перезапуска стоит в [Service]. Это отдельный, более верхнеуровневый защитный механизм — он срабатывает вне зависимости от Restart=on-failure, Restart=always или Restart=on-abnormal.

Restart=Когда перезапускаетПопадает под StartLimitBurst
no (по умолчанию)НикогдаНе актуально — рестартов не бывает
on-failureНенулевой код возврата, сигнал, таймаутДа
on-abnormalСигнал, таймаут, watchdogДа
alwaysЛюбое завершение, включая штатную остановкуДа
on-watchdogТолько по watchdog-таймаутуДа

Отдельно стоит RestartSec — пауза перед следующей попыткой перезапуска после падения. Она не заменяет StartLimitBurst, а работает вместе с ним: RestartSec определяет, как быстро сервис пытается подняться снова, а StartLimitBurst/StartLimitIntervalSec — сколько таких попыток можно сделать, прежде чем systemd остановится. Слишком маленький RestartSec при дефолтном окне в 10 секунд означает, что лимит выгорает почти мгновенно — если RestartSec=1s, а StartLimitBurst=5, у сервиса есть буквально 5 секунд на то, чтобы либо подняться, либо навсегда замолчать.

В части относительно новых версий systemd (проверьте man systemd.service на вашей системе — директива могла появиться не во всех дистрибутивах, которые ещё в эксплуатации) есть RestartSteps= и RestartMaxDelaySec=, позволяющие задать нарастающую паузу между попытками вместо фиксированной. Это удобно для сервисов, которые падают из-за временной недоступности зависимости (БД, сеть) — растущий интервал даёт зависимости больше шансов подняться, а не долбит её попытками каждую секунду.

Как правильно настроить лимиты под конкретный сервис

Дефолтные 5 попыток за 10 секунд — разумный компромисс для типового демона, но не универсальный. Менять параметры стоит осознанно, а не просто увеличивать «про запас».

Правьте не сам unit-файл дистрибутива, а override:

systemctl edit myapp.service

Это откроет редактор для дроп-ина в /etc/systemd/system/myapp.service.d/override.conf. Пример разумной настройки для сервиса, который может ждать медленную БД при старте:

[Unit]
StartLimitIntervalSec=120
StartLimitBurst=8

[Service]
Restart=on-failure
RestartSec=10s

После правки:

systemctl daemon-reload
systemctl restart myapp.service

Несколько практических ориентиров, а не жёстких правил:

  • Для критичных сервисов с быстрым стартом (веб-сервер, простой API) обычно достаточно дефолтных значений — если он не может подняться за 5 попыток в 10 секунд, дело не в скорости рестартов, а в конфигурации или коде.
  • Для сервисов с зависимостями, которые сами долго стартуют (БД, очереди сообщений) имеет смысл увеличить StartLimitIntervalSec и/или RestartSec, чтобы дать зависимости время подняться, вместо того чтобы упереться в лимит раньше, чем зависимость успеет ответить.
  • Полностью отключать лимит (StartLimitIntervalSec=0) — плохая идея почти всегда: без него сервис, который падает из-за реального бага, будет молотить рестартами бесконечно, нагружая CPU и забивая журнал десятками записей в секунду. Если очень нужно снять ограничение на время отладки — снимайте его временно и осознанно, не оставляйте так в проде.
  • Не забывайте, что Type= тоже влияет на то, что считается «успешным стартом». Если у сервиса Type=notify, а приложение никогда не шлёт sdnotify, systemd будет ждать таймаут готовности при каждой попытке — это можно спутать с лимитом рестартов, хотя причина другая. Про это подробнее в статье о том, как systemd понимает, что сервис поднялся.

Диагностика: лимит рестартов или реальная ошибка сервиса

Порядок действий, который экономит время в момент, когда сервис «не запускается» и непонятно почему.

1. Посмотрите на Result в systemctl status.

systemctl status myapp.service

Если там Result: start-limit-hit — вы имеете дело с защитным лимитом, а не с последней ошибкой приложения. Любой другой результат (exit-code, signal, timeout) — это уже про сам сервис.

2. Найдите первое падение в цепочке, а не последнее.

journalctl -u myapp.service --since "-30min" --no-pager | less

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

3. Проверьте текущий счётчик и лимиты.

systemctl show myapp.service -p NRestarts -p StartLimitBurst -p StartLimitIntervalSec

NRestarts покажет, сколько раз сервис перезапускался с последнего сброса счётчика — если это число близко к StartLimitBurst, вы на грани лимита или уже за ним.

4. Сбросьте состояние после того, как исправили причину.

systemctl reset-failed myapp.service
systemctl start myapp.service

Это ключевой момент, который часто упускают: даже если вы нашли и исправили первопричину падений (поправили конфиг, освободили место на диске, дождались доступности БД), сам юнит остаётся в failed, пока вы явно не сбросите его командой reset-failed или не подождёте, пока истечёт StartLimitIntervalSec с момента последней попытки. Простой systemctl restart после этого может снова тут же упереться в лимит, если окно ещё не истекло, и это выглядит так, будто исправление не помогло — хотя проблема уже решена, просто лимит ещё не сброшен.

5. Если событие разовое — проверьте систему целиком.

Иногда «сервис не поднимается» — симптом более общей проблемы: диск заполнен, кончились файловые дескрипторы, упал сетевой интерфейс. Тогда в лимит рестартов может упасть сразу несколько несвязанных юнитов — это диагностический сигнал сам по себе. Общий подход к чтению логов для таких случаев разобран в статье как читать логи и находить причину сбоя.

Типичные грабли эксплуатации

Автоматика перезапускает сервис вручную, не проверяя reset-failed. Скрипты деплоя или Ansible-плейбуки, которые дёргают systemctl restart в цикле retry при недоступности сервиса, сами расходуют лимит рестартов и переводят юнит в failed — хотя без этих внешних попыток сервис, возможно, поднялся бы с первого раза.

Мониторинг видит failed и снова дёргает restart, не разбираясь в причине. Healthcheck-скрипт, который при недоступности сервиса всегда делает systemctl restart, при start-limit-hit просто тратит ещё одну попытку впустую (юнит тут же снова уходит в failed, пока окно не истекло) и создаёт иллюзию активности без результата. Правильнее сначала проверить Result, и если это start-limit-hit — идти разбирать первопричину, а не долбить restart.

StartLimitIntervalSec=0 оставлен «на всякий случай» после отладки. Снятый на время лимит забывают вернуть, и сервис, который начинает падать в проде из-за реального бага, крутится в рестартах бесконечно вместо того, чтобы остановиться и подать явный сигнал через failed-статус, который ловит мониторинг.

Каскад рестартов через зависимости юнитов. Если сервис A требует сервис B (Requires=), а B периодически падает и уходит в свой start-limit-hit, то A будет падать вслед за ним при каждой попытке старта — и у A параллельно расходуется свой лимит. В логах это выглядит как две независимые проблемы, а на деле — одна.

Счётчик обнуляется при перезагрузке. StartLimitBurst привязан к процессу systemd (PID 1) и не сохраняется на диск — после reboot счётчик сбрасывается сам. Сервис, который был в failed из-за лимита, после перезагрузки сервера снова стартует штатно и «сам себя чинит» — что маскирует нерешённую первопричину до следующего цикла падений.

Лимиты рестартов не только в systemd

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

Docker / docker compose. Политика restart: unless-stopped или restart: on-failure заставляет Docker перезапускать контейнер, но у него нет прямого аналога StartLimitBurst — вместо жёсткого лимита Docker увеличивает паузу между последовательными перезапусками (backoff), не переводя контейнер в постоянное failed-состояние само по себе. При on-failure:N (с числом) есть ограничение на количество попыток, но без числа перезапуски могут продолжаться значительно дольше, чем в systemd. Разбор смежных лимитов контейнеров — в статье про Docker healthcheck: зависший, но не упавший процесс — отдельная категория проблемы, которую одним Restart= не поймать.

supervisord. Использует startretries (по умолчанию небольшое фиксированное число) и startsecs — минимальное время, которое процесс должен проработать, чтобы попытка считалась успешной. Если процесс падает быстрее startsecs раз за разом больше, чем startretries, supervisord переводит его в состояние FATAL — по смыслу это прямой аналог failed в systemd.

PM2. Аналогично использует max_restarts и min_uptime — комбинацию количества попыток и минимального времени жизни процесса, после превышения которой процесс помечается как остановленный (stopped) и не перезапускается автоматически.

Общий принцип везде один: любой вменяемый supervisor должен уметь остановиться сам, а не долбить упавший процесс бесконечно — вопрос только в том, какими параметрами это регулируется и как выглядит сообщение об исчерпанном лимите в конкретном инструменте.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро узнать, что причина недоступности — именно лимит рестартов, а не сбой приложения?

Одна команда: systemctl status <service> — если в выводе Active: failed (Result: start-limit-hit), это лимит. Любой другой Result (exit-code, signal, core-dump, timeout) означает, что systemd честно исчерпал попытки, но причину нужно искать в логах последнего запуска, а не в самом факте failed.

systemctl restart после исправления бага не помогает — сервис снова падает в failed. Почему?

Скорее всего, StartLimitIntervalSec ещё не истёк с последней серии попыток, и restart сам по себе засчитывается как ещё одна попытка внутри того же окна. Выполните systemctl reset-failed <service> перед следующим start, это сбрасывает счётчик вручную.

Можно ли посмотреть, сколько раз сервис уже перезапускался, не залезая в journalctl?

Да: systemctl show <service> -p NRestarts покажет число перезапусков с момента последнего сброса счётчика (реюза, reset-failed или перезагрузки системы).

Стоит ли ставить StartLimitBurst в большое число, чтобы «сервис точно поднялся»?

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

Лимит применяется на уровне всей системы или на каждый сервис отдельно?

На каждый юнит отдельно, если явно не указано иное. StartLimitIntervalSec/StartLimitBurst, заданные в override одного сервиса, не влияют на лимиты других юнитов — у каждого свой независимый счётчик.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →