systemd show: причина перезапуска сервиса
Почему перезапустился systemd-сервис: status молчит, журнал признаётся. Пять кодов смерти с живого стенда — и одна смерть, которую система называет успехом.
Мы убили один сервис пятью разными способами на одноразовой Arch-VM и дали systemd допросить самого себя. Каждая строка ниже — собственный вывод журнала, напечатанный без правок: четыре признания, одно молчание и одно «сдался», о чём вывод status никогда не сообщает. Если вы пришли из нашего недавнего вскрытия сервисов, отказывающихся запускаться, то это комната после захлопнувшейся двери: сервис запустился, поработал, умер — и вы хотите знать, кто его убил.
Как показать причину, по которой перезапустился systemd-сервис?
Три команды, именно в этом порядке. Первая читает ленту, вторая — счётчик, третья фильтрует ленту так, чтобы остались одни смерти.
# the confession, most recent first
journalctl -u myservice.service -n 50 --no-pager
# the counter and the last death, machine-readable
systemctl show myservice.service -p NRestarts -p ExecMainStatus \
-p ExecMainCode -p Result
# every death, timestamped to the millisecond
journalctl -u myservice.service --grep 'Main process exited' \
-o short-precise | tail -10
Проверка: на нашей стендовой VM вторая команда ответила Result=exit-code, NRestarts=3, ExecMainStatus=1, ExecMainCode=1 для сервиса, погибшего трижды за несколько секунд до этого. Last tested 2026-11-02.
Чего чаще всего не замечают — это NRestarts. systemctl status обнуляет свою историю при каждом перезапуске, поэтому всегда кажется, что сервис «только что запустился». Счётчик не обнуляется никогда. На стенде status докладывал active (running), 10 ms old — а счётчик строкой ниже говорил restart counter is at 3. Этот зазор и есть вся отладка: у сервиса есть криминальное досье, а status показывает его свидетельство о рождении.
Почему systemctl status молчит о причине перезапуска сервиса?
Потому что status — снимок текущего момента, а журнал — плёнка камеры наблюдения. Вот снимок, снятый на стенде, пока сервис умирал каждые ~1.2 секунды:
● flaky.service - dies on purpose for the journal
Loaded: loaded (/etc/systemd/system/flaky.service; static)
Active: active (running) since Mon 2026-11-02 05:05:46 UTC; 10ms ago
Main PID: 627 ((die.sh))
CPU: 3ms
Nov 02 05:05:46 bench-arch systemd[1]: flaky.service: Scheduled restart
job, restart counter is at 3.
status — снимок момента; журнал — плёнка камеры наблюдения.
Что значат code=exited и status=11/SEGV в журнале?
Каждая смерть оставляет в журнале две строки — строку кода и вердикт. Мы вызвали все пять видов намеренно. Вот строки-признания, без правок:
галерея признаний — по одному на смерть, прямо со стенда# death 1: the app exits nonzero
flaky.service: Main process exited, code=exited, status=1/FAILURE
flaky.service: Failed with result 'exit-code'.
# death 2: the app segfaults (kill -SEGV)
flaky.service: Main process exited, code=dumped, status=11/SEGV
# death 3: the app aborts (kill -ABRT) — assert died, or a
# watchdog inside the app pulled the plug
flaky.service: Main process exited, code=dumped, status=6/ABRT
# death 4: ExecStart points at a file that does not exist —
# the app never ran
(nothere)[1260]: flaky.service: Failed at step EXEC spawning
/usr/local/bin/nothere: No such file or directory
flaky.service: Main process exited, code=exited, status=203/EXEC
# death 5: the app kills itself with SIGTERM (kill -TERM)
flaky.service: Deactivated successfully.
Правило разбора: code= говорит, как процесс ушёл (exited = вышел сам, dumped = вынесли ногами вперёд), status= называет род смерти, а Failed with result — краткое резюме systemd в одну строку. Таблица — полный дешифратор того, что мы поймали:
Что говорит журнал Что произошло на самом деле Куда смотреть дальше
code=exited, status=1/FAILURE приложение решило умереть само — плохой конфиг, занятый порт, не хватает переменной окружения логи приложения над строкой выхода в journalctl -u NAME
code=dumped, status=11/SEGV сегфолт — ошибка памяти в приложении или нативной зависимости coredumpctl list — там стектрейс
code=dumped, status=6/ABRT abort — сорвавшийся assert или сдавшийся внутренний watchdog собственный лог приложения, строкой до abort
status=203/EXEC путь в ExecStart неверный или не исполняемый — приложение даже не стартовало проверьте путь, chmod +x, shebang интерпретатора
Deactivated successfully (строки о неудаче нет) смерть по SIGTERM — по умолчанию считается чистым выходом § ниже; это и есть фантом
start request repeated too quickly / start-limit-hit systemd сдался перезапускать — сработал лимит всплесков сначала почините смерть; лимит трогайте только после
Почему systemd-сервис перезапускается каждые несколько секунд?
Потому что Restart= делает свою работу, а смерти не прекращаются. Ритм задаёт RestartSec (по умолчанию 100 мс), терпение — StartLimitIntervalSec/StartLimitBurst (по умолчанию 5 неудач за 10 с). На стенде с RestartSec=1 три смерти легли так:
ритм — RestartSec=1, три смерти за три секунды05:05:43 flaky.service: Scheduled restart job, restart counter is at 1.
05:05:44 flaky.service: Scheduled restart job, restart counter is at 2.
05:05:46 flaky.service: Scheduled restart job, restart counter is at 3.
# and when the burst limit applies, the loop ends like this:
flaky.service: Start request repeated too quickly.
flaky.service: Failed with result 'start-limit-hit'.
Скорость собственного цикла считайте одной командой:
# restarts in the last hour, with timestamps
journalctl -u myservice.service --since "1 hour ago" \
--grep 'Scheduled restart' --no-pager | tail -20
Проверка: на стенде команда напечатала те три строки выше; счётчик сбросился на 1, как только прокрутилось 10-секундное окно лимита. Last tested 2026-11-02.
Ловушка «заглушить сигнализацию» заслуживает отдельного предупреждения: поднять StartLimitBurst — не значит починить умирающий сервис, вы лишь чаще прячете труп. Чините смерть — таблица-дешифратор выше подсказывает, куда смотреть, — а к лимиту относитесь как к пожарной сигнализации, чем он и является.
Почему сервис остановился, а журнал докладывает об успехе?
Именно этого мы не ожидали — ради этого случая пост и существует. Пятая смерть: процесс убил себя по SIGTERM. Перезапуска не последовало. Строки о неудаче не появилось. Весь комментарий журнала — Deactivated successfully, а systemctl show ответил Result=success, ExecMainCode=0, ExecMainStatus=0 — убийство, оформленное как чистый выход на пенсию.
Причина живёт в man-странице, а не в журнале: дефолтный список SuccessExitStatus включает SIGTERM (а заодно SIGINT, SIGPIPE и статус 0). Любой процесс, погибший по SIGTERM, для политики перезапуска — успех: Restart=on-failure не срабатывает, и ни одна строка лога не называет это смертью. Если ваш сервис живёт под супервизором или его завершает внешний watchdog, — вот это молчание вы и разглядывали.
Два честных выхода — смотря чего вы хотите:
Метод стенда: повторите пять смертей сами
Одноразовая Arch-VM (quickemu, 2 vCPU, без изменений на хосте). Один юнит, один скрипт: /usr/local/bin/die.sh принимает вид смерти аргументом — exit 1, kill -SEGV $$, kill -ABRT $$, kill -TERM $$ — либо ExecStart, указывающий на несуществующий путь ради 203-й. Между смертями меняете ExecStart юнита, daemon-reload, старт, даёте умереть трижды, стоп, фиксация. Полная чековая книга без единой правки: пять признаний, пожимание плечами, ритм, капитуляция и фантом — строки журнала выше выбраны из неё дословно. Коробка процессорная, никакого GPU рядом, ничего не сохранено: клон уничтожен в ту же сессию.
Честный итог: coredumps мы не разбирали (coredumpctl — это отдельный вечер), а стендовые имена поддельные — в ваших строках journalctl будут ваши юниты. Что устояло: четыре смерти из пяти признаются через code=, status= и строку результата; счётчик не забывает никогда; а пятая смерть — SIGTERM — в журнале числится успехом. Молчание журнала — само по себе признание. status продолжит пожимать плечами; читайте ленту.