Debian 13 «Запит на запуск повторювався занадто швидко»: виправлення systemd, які справді працюють

Було корисно?

Ви розгортаєте сервіс. Він падає. Ви виправляєте очевидну річ. Він падає знову — швидше. Потім systemd викидає прокляту строку: «Запит на запуск повторювався занадто швидко» і перестає взагалі намагатися. Тепер ви в пастці: сервіс не лише не працює, ваш init-систем ввічливо відмовляється дозволити вам ще раз увійти в ту саму помилку.

Це не «містика» systemd. Він робить саме те, для чого був створений: обмежує частоту флапінгу юнітів, щоб один поганий сервіс не перетворив хост на лог-і CPU-грі́йку. Трюк у діагностиці реальної помилки, а потім у застосуванні рішень, які переживуть оновлення, перезавантаження і майбутнього вас о 2:00 ночі.

Що насправді означає «Запит на запуск повторювався занадто швидко»

Systemd каже вам: «Я намагався запустити цей юніт декілька разів за короткий проміжок. Він постійно падав. Я припиняю спроби, поки ви не втрутитеся». Втручання може бути налаштуванням конфігу, виправленням залежності або свідомою зміною політики перезапуску юніта. Іноді також потрібно очистити стан помилки.

Під капотом systemd застосовує обмеження частоти запуску для кожного юніта. Якщо юніт переходить у стан відмови занадто багато разів у межах налаштованого інтервалу, systemd спрацьовує і припиняє автоматичні спроби запуску. За замовчуванням поведінка залежить від версії та політик дистрибутива, але основний механізм один: StartLimitIntervalSec + StartLimitBurst. Вдарите по цій комбінації — отримаєте повідомлення.

Важливий нюанс: це не тільки про Restart=. Навіть юніти, що самі не перезапускаються, можуть потрапити під ліміт, якщо щось (залежність, таймер, адміністратор, що багаторазово виконує systemctl start) часто ініціює спроби.

Що робити: розглядайте повідомлення як симптом циклу. Сам цикл — проблема. Ліміт — ремінь безпеки.

Цитата, бо вона досі актуальна: «Сподівання — не стратегія.» — Джин Кранс

Швидкий план діагностики (перший/другий/третій)

Якщо хочете швидко знайти вузьке місце — не починайте з редагування файлів юніта. Почніть із трьох питань: Що зламалося? Чому воно зламалося? Хто не дає спокою і повторює спроби?

Перший крок: підтвердіть, що саме ліміт припинив повторні спроби

  • Шукайте результати з start-limit-hit і фактичний код виходу процесу сервісу.
  • Перевірте, чи сервіс завершується миттєво (поганий бінарник, неправильний конфіг) або зависає до таймауту (блок, недоступна залежність).

Другий крок: знайдіть першу реальну помилку в логах

  • Використовуйте journalctl -u з вузьким часовим вікном.
  • Шукайте найранішу помилку в серії перезапусків, не тільки останній рядок. Останній часто — це просто systemd, який здався.

Третій крок: виявте тригер циклу

  • Чи встановлено у юніта Restart=always?
  • Чи це ланцюжок залежностей, де батько постійно намагається?
  • Чи таймер або path-юніт його викликає?
  • Чи щось зовнішнє багаторазово виконує systemctl start (або агент управління конфігами)?

Швидкі перемоги зазвичай приходять від: виправлення реальної причини, а потім підгонки семантики перезапуску так, щоб відмова не перетворювалася на самооборонну відмову в обслуговуванні хоста.

Практичні завдання: команди, виводи, рішення (12+)

Нижче — перевірені в полі завдання. Кожне містить: команду, реалістичний вивід, що це означає, і як треба діяти далі.

Завдання 1: Перевірити стан юніта і підтвердити start-limit-hit

cr0x@server:~$ systemctl status myapp.service
× myapp.service - MyApp API
     Loaded: loaded (/lib/systemd/system/myapp.service; enabled; preset: enabled)
    Drop-In: /etc/systemd/system/myapp.service.d
             └─override.conf
     Active: failed (Result: start-limit-hit) since Mon 2025-12-29 09:12:11 UTC; 2min 3s ago
   Duration: 220ms
    Process: 18934 ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml (code=exited, status=1/FAILURE)
   Main PID: 18934 (code=exited, status=1/FAILURE)

Dec 29 09:12:11 server systemd[1]: myapp.service: Scheduled restart job, restart counter is at 5.
Dec 29 09:12:11 server systemd[1]: myapp.service: Start request repeated too quickly.
Dec 29 09:12:11 server systemd[1]: myapp.service: Failed with result 'start-limit-hit'.
Dec 29 09:12:11 server systemd[1]: Failed to start myapp.service - MyApp API.

Значення: systemd обмежив частоту запуску юніта. Сам сервіс завершується з кодом 1. Цикл перезапусків досяг 5 спроб (типовий поріг).

Рішення: не поспішайте чіпати ліміти перезапуску. У вас справжня проблема в додатку. Ідіть у логи за першою помилкою.

Завдання 2: Витягти логи навколо вікна відмови (і не читати лише останній рядок)

cr0x@server:~$ journalctl -u myapp.service -b --since "10 minutes ago" --no-pager
Dec 29 09:12:10 server myapp[18934]: ERROR: cannot read config file: /etc/myapp/config.yml: permission denied
Dec 29 09:12:10 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
Dec 29 09:12:10 server systemd[1]: myapp.service: Failed with result 'exit-code'.
Dec 29 09:12:11 server systemd[1]: myapp.service: Scheduled restart job, restart counter is at 1.
Dec 29 09:12:11 server systemd[1]: Started myapp.service - MyApp API.
Dec 29 09:12:11 server myapp[18938]: ERROR: cannot read config file: /etc/myapp/config.yml: permission denied
Dec 29 09:12:11 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
Dec 29 09:12:11 server systemd[1]: myapp.service: Start request repeated too quickly.

Значення: Реальна помилка — проблема з правами доступу до конфіг-файлу. Ліміт — лише наслідок.

Рішення: виправте власника/права файлу або User/Group у юніті. Не збільшуйте поріг, щоб «забрати» проблему.

Завдання 3: Перевірити фактичну конфігурацію юніта (що systemd насправді запускає)

cr0x@server:~$ systemctl cat myapp.service
# /lib/systemd/system/myapp.service
[Unit]
Description=MyApp API
After=network-online.target
Wants=network-online.target

[Service]
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
Restart=on-failure
RestartSec=1

[Install]
WantedBy=multi-user.target

# /etc/systemd/system/myapp.service.d/override.conf
[Service]
RestartSec=250ms

Значення: Мається drop-in override з RestartSec=250ms. Це змушує флапаючі сервіси швидко потрапляти під ліміт.

Рішення: тримайте затримку перезапуску розумною (2–10 секунд), якщо немає дуже вагомої причини для іншого. Логи і диски вам подякують.

Завдання 4: Визначити налаштування ліміту запуску (за замовчуванням vs перекриття)

cr0x@server:~$ systemctl show myapp.service -p StartLimitIntervalSec -p StartLimitBurst -p Restart -p RestartUSec
StartLimitIntervalSec=10s
StartLimitBurst=5
Restart=on-failure
RestartUSec=250ms

Значення: З інтервалом 10 секунд і burst=5, затримка 250ms може практично миттєво спровокувати ліміт.

Рішення: виправте реальну помилку; потім встановіть RestartSec=2s або більше, якщо сервіс не безпечний для агресивних рестартів.

Завдання 5: Скинути стан failed (тільки після внесених змін)

cr0x@server:~$ systemctl reset-failed myapp.service
cr0x@server:~$ systemctl start myapp.service
cr0x@server:~$ systemctl status myapp.service
● myapp.service - MyApp API
     Loaded: loaded (/lib/systemd/system/myapp.service; enabled; preset: enabled)
    Drop-In: /etc/systemd/system/myapp.service.d
             └─override.conf
     Active: active (running) since Mon 2025-12-29 09:16:42 UTC; 3s ago
   Main PID: 19501 (myapp)
      Tasks: 8
     Memory: 34.2M
        CPU: 210ms
     CGroup: /system.slice/myapp.service
             └─19501 /usr/local/bin/myapp --config /etc/myapp/config.yml

Значення: Ліміт запуску блокував старти; reset-failed це очистив. Тепер сервіс працює.

Рішення: якщо після скидання він все ще падає, ви не вирішили корінну проблему. Поверніться до логів.

Завдання 6: Підтвердити, що користувач сервісу може читати необхідне

cr0x@server:~$ sudo -u myapp test -r /etc/myapp/config.yml && echo OK || echo NO
NO
cr0x@server:~$ ls -l /etc/myapp/config.yml
-rw------- 1 root root 912 Dec 29 08:58 /etc/myapp/config.yml

Значення: Конфіг доступний тільки root. Якщо сервіс працює як User=myapp, йому недоступно читання.

Рішення: змініть власника/групу, відрегулюйте права або запускайте сервіс під іншим користувачем (як крайній захід).

Завдання 7: Виявити відмови в ланцюжку залежностей (проблема «це не мій юніт»)

cr0x@server:~$ systemctl list-dependencies --reverse myapp.service
myapp.service
● myapp.target
● multi-user.target

Значення: Ніхто особливий його не тягне; ймовірно він увімкнений напряму.

Рішення: якщо список зворотних залежностей показує більший ланцюжок (як-от таргет або інший сервіс), можливо, треба виправити батьківський юніт також.

Завдання 8: Визначити, чи таймер або path-юніт багаторазово тригерить сервіс

cr0x@server:~$ systemctl list-timers --all | grep -E 'myapp|myapp-'
Mon 2025-12-29 09:20:00 UTC  2min 10s left Mon 2025-12-29 09:10:00 UTC  7min ago myapp-refresh.timer myapp-refresh.service

cr0x@server:~$ systemctl status myapp-refresh.timer
● myapp-refresh.timer - MyApp refresh timer
     Loaded: loaded (/lib/systemd/system/myapp-refresh.timer; enabled; preset: enabled)
     Active: active (waiting) since Mon 2025-12-29 08:10:00 UTC; 1h 10min ago
    Trigger: Mon 2025-12-29 09:20:00 UTC; 2min 10s left

Значення: Інший юніт може регулярно викликати ваш сервіс (або пов’язаний із ним). Невдалий job оновлення може виглядати як «флапінг myapp», якщо вони ділять ресурси.

Рішення: якщо таймер занадто агресивний або зламаний — виправте таймер або вимкніть його до стабілізації.

Завдання 9: Перевірити коди виходу та причини сигналів між рестартами

cr0x@server:~$ systemctl show myapp.service -p ExecMainStatus -p ExecMainCode -p ExecMainStartTimestamp -p ExecMainExitTimestamp
ExecMainStatus=1
ExecMainCode=exited
ExecMainStartTimestamp=Mon 2025-12-29 09:12:10 UTC
ExecMainExitTimestamp=Mon 2025-12-29 09:12:10 UTC

Значення: Воно завершується миттєво. Зазвичай це конфіг, права, відсутні файли, відсутні бібліотеки або «не може прив’язати порт». Це не повільна залежність.

Рішення: пріоритезуйте валідацію конфігу та проблеми оточення; не витрачайте час на збільшення TimeoutStartSec.

Завдання 10: Підтвердити порти та припущення socket activation

cr0x@server:~$ ss -lntp | grep ':8080'
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*    users:(("nginx",pid=1201,fd=6))

Значення: Хтось інший вже займає порт 8080 (тут nginx). Якщо myapp очікує прив’язати його, він впаде в цикл.

Рішення: змініть порт прослуховування myapp, налаштуйте nginx як проксі або використайте socket activation systemd правильно (не напівзаходами).

Завдання 11: Перевірити синтаксис юніта і приховані опечатки

cr0x@server:~$ systemd-analyze verify /etc/systemd/system/myapp.service
/etc/systemd/system/myapp.service:12: Unknown lvalue 'RestartSecs' in section 'Service'

Значення: Systemd ігнорував неправильно написану директиву. Ви думали, що маєте затримку перезапуску; а її немає.

Рішення: виправте опечатку, виконайте daemon-reload і перевірте systemctl show для ефективних значень.

Завдання 12: Перевірити оточення, яке бачить сервіс (PATH, змінні, робоча директорія)

cr0x@server:~$ systemctl show myapp.service -p Environment -p EnvironmentFile -p WorkingDirectory -p User -p Group
Environment=
EnvironmentFile=
WorkingDirectory=/
User=myapp
Group=myapp

Значення: Немає встановленого оточення, робоча директорія — root. Якщо додаток очікує відносні шляхи, він може впасти відразу.

Рішення: встановіть WorkingDirectory= та явні Environment= або EnvironmentFile=. Не покладайтеся на інтерективні shell-значення.

Завдання 13: Знайти, хто що змінив (drop-ins, vendor units, overrides)

cr0x@server:~$ systemctl show myapp.service -p FragmentPath -p DropInPaths
FragmentPath=/lib/systemd/system/myapp.service
DropInPaths=/etc/systemd/system/myapp.service.d/override.conf

Значення: Vendor-юніт у /lib, локальний override у /etc. Це правильний підхід на Debian.

Рішення: ніколи не редагуйте /lib/systemd/system/*.service напряму. Поміщайте зміни в drop-ins, щоб вони пережили оновлення пакетів.

Завдання 14: Відстежити «працює вручну, але не як сервіс»

cr0x@server:~$ sudo -u myapp /usr/local/bin/myapp --config /etc/myapp/config.yml
ERROR: cannot open database socket /run/postgresql/.s.PGSQL.5432: no such file or directory

Значення: Запущений під користувачем сервісу, він не може дістатися залежності (Unix-сокет PostgreSQL). Можливо, postgres не запущений, або слухає в іншому місці, або права блокують доступ.

Рішення: виправте готовність залежностей і конфіг; потім змінюйте порядок (After=) лише за потреби.

Завдання 15: Візуалізувати порядок завантаження і критичний ланцюжок (повільні старти та таймаути)

cr0x@server:~$ systemd-analyze critical-chain myapp.service
myapp.service +4.212s
└─network-online.target +4.198s
  └─systemd-networkd-wait-online.service +4.150s
    └─systemd-networkd.service +1.021s
      └─systemd-udevd.service +452ms

Значення: Ваш юніт залежить від network-online.target, який чекає на мережу. Це може бути нормально — або пастка при завантаженні.

Рішення: якщо сервіс насправді не потребує «online», перемкніть на After=network.target і видаліть wait-online залежність. Менше несподіванок при завантаженні.

Виправлення systemd, які дійсно прилипають (і чому)

«Виправлення, які прилипають» мають дві риси: переживають оновлення і відображають поведінку сервісу в реалі. Більшість флапаючих сервісів або неправильно описані (неправильна семантика юніта), або зламані в runtime (конфіг, права, залежності). Правильна відповідь зазвичай — невеликий override drop-in плюс реальна правка в середовищі додатку.

Використовуйте drop-ins, а не редагування в /lib

На Debian пакетно-управляємі файли юнітів живуть у /lib/systemd/system. Локальні зміни належать у /etc/systemd/system. Якщо ви редагуєте файли у /lib, апгрейд пакета рано чи пізно «виправить» вашу правку.

cr0x@server:~$ sudo systemctl edit myapp.service
# (editor opens)

Додайте drop-in на кшталт цього:

cr0x@server:~$ cat /etc/systemd/system/myapp.service.d/override.conf
[Service]
Restart=on-failure
RestartSec=5s
TimeoutStartSec=30s

[Unit]
StartLimitIntervalSec=60s
StartLimitBurst=3

Чому це прилипне: воно в /etc, отже оновлення не перезапишуть. Також це знижує ймовірність, що ваш сервіс буде тероризувати хост: 5 секунд між спробами і лише 3 спроби на хвилину перед тим, як systemd зупиниться й попросить людину втрутитися.

Чого уникати: встановлення StartLimitBurst=1000 або RestartSec=100ms, бо «хочу, щоб він швидко відновлювався». Це не відновлення; це форк-бомба з кращою брендовою назвою.

Змусьте логіку перезапуску відповідати режимам відмов

Більшість сервісів не потребують Restart=always. Використовуйте його тільки для демонів, які мають бути завжди присутні й безпечні для перезапуску незалежно від коду виходу. Віддавайте перевагу:

  • Restart=on-failure для типових серверів, які можуть виходити під час оновлень або перезавантаження конфігу.
  • Restart=no для одноразових завдань, які повинні голосно провалитися і зупинитися.
  • Restart=on-abnormal якщо хочете рестарти при сигналах або дампах, а не при чистих ненульових виходах (корисно в деяких шаблонах).

Виправте Type= і готовність, або systemd «допоможе» вам в зацикленні

Ще один класичний випадок: сервіс насправді в порядку, але systemd думає, що він не стартував. Це трапляється, коли Type= вказано неправильно.

  • Type=simple (за замовчуванням): процес стартує і лишається у foreground. Більшість додатків підходять.
  • Type=forking: застарілі демони, що форкають у фон. Якщо ви ставите це для foreground-додатку, systemd може неправильно читати стан і вбивати/перезапускати його.
  • Type=notify: додаток має викликати sd_notify. Якщо не робить — systemd може чекати таймаут і рестартувати.
  • Type=oneshot: завдання, що запускаються і виходять; комбінуйте з RemainAfterExit=yes коли потрібно.

Встановіть тип правильно. Якщо ви не контролюєте додаток і він не підтримує sd_notify — не робіть вигляд, що підтримує.

Використовуйте ExecStartPre для валідації, але не зловживайте

ExecStartPre= — хороше місце для перевірки конфігу перед спробою старту. Зроблено правильно — воно запобігає флапінгу. Зроблено неправильно — створює флапінг.

Приклад: перевірити, що конфіг існує і читабельний для користувача сервісу:

cr0x@server:~$ cat /etc/systemd/system/myapp.service.d/validate.conf
[Service]
ExecStartPre=/usr/bin/test -r /etc/myapp/config.yml

Якщо цей тест падає, юніт відпаде швидко з чіткою причиною. Політика перезапуску має тут бути консервативною — не хочете, щоб хост забивали 20 помилками в секунду через відсутній файл.

Явно вказуйте залежності, але не надмірно посилюйте серіалізацію завантаження

Додавання After=postgresql.service допоможе, якщо ваш додаток дійсно не може стартувати, поки БД не буде готова. Але багато команд механічно додають «чекати мережу online» і «чекати все підряд». Тоді повільний DHCP збиває половину завантаження.

Практична позиція:

  • Використовуйте Wants= для м’яких залежностей, від яких можна деградувати.
  • Використовуйте Requires= для жорстких залежностей. Але знайте: якщо залежність падає — ваш юніт теж впаде.
  • Краще, щоб ваш додаток сам реалізовував повторні спроби для upstream-сервісів (БД, API), поки процес залишається запущеним, ніж systemd весь час його перезапускав.

Жарт #1: Цикли перезапусків як корпоративні загальні збори — багато активності, ніякого прогресу, і всі йдуть більш втомленими.

Знайте, коли варто регулювати StartLimit* (і коли ні)

Є легітимні випадки для зміни лімітів запуску:

  • Сервіси, що можуть коротко падати під час технічних робіт upstream і безпечно повторюватися протягом хвилин.
  • Агенти, що підключаються до віддалених кінцевих точок і іноді змагаються при завантаженні.

Але зміна лімітів не вирішує «permission denied» або «некоректний конфіг». У таких випадках збільшення повторів просто перетворює помилку на суцільний шум.

Використовуйте systemd-run для безпечного відтворення

Якщо потрібно відтворити середовище сервісу без редагування фактичного юніта, systemd-run може створити транієнтні юніти з подібними властивостями. Це хороший спосіб перевірити припущення щодо User, WorkingDirectory і Environment.

cr0x@server:~$ sudo systemd-run --unit=myapp-test --property=User=myapp --property=WorkingDirectory=/var/lib/myapp /usr/local/bin/myapp --config /etc/myapp/config.yml
Running as unit: myapp-test.service
cr0x@server:~$ systemctl status myapp-test.service
● myapp-test.service - /usr/local/bin/myapp --config /etc/myapp/config.yml
     Loaded: loaded (/run/systemd/transient/myapp-test.service; transient)
     Active: failed (Result: exit-code) since Mon 2025-12-29 09:22:06 UTC; 1s ago
    Process: 20411 ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml (code=exited, status=1/FAILURE)

Рішення: якщо воно падає так само, це не «магія systemd». Це середовище виконання.

Типові режими відмов за повідомленням

Ось що найчастіше бачу в Debian-флотах. Повідомлення ліміту — лише охоронець. А ці — п’яні гості.

1) Невідповідність прав і власності

Сервіс працює не від root. Конфіг-файли, сокети або директорії стану — root-owned. Бінарник виходить миттєво з корисною помилкою, але юніт перезапускається занадто швидко, потрапляє під ліміт, і ви дивитесь не на ту строку.

2) Неправильний Type сервісу

Foreground-програма позначена як forking, або notify без підтримки нотифікацій. Systemd думає, що вона не стартувала, чекає таймаут, вбиває, рестартує, повторює.

3) Швидкий краш через відсутню залежність

Приклад: відсутній сокет бази даних під час завантаження. Додаток виходить швидко; systemd перезапускає; повтор. Виправте порядок або зробіть додаток толерантним до недоступності.

4) Збої прив’язки (bind failures)

Порт уже зайнятий, або привілейований порт без потрібних можливостей, або неправильна адреса прослуховування. Це падіння миттєве і може зациклитися при малій затримці рестарту.

5) ExecStart вказує на обгортку зі злим шебангом

/bin/bash може не існувати в мінімальних контейнерах. Або скрипт використовує set -e і завершується через відсутню змінну. Systemd охоче його перезапускає, поки він не втомиться.

6) Таймаути і повільні старти

Процес стартує, але не стає «готовим» до TimeoutStartSec. Часто в парі з неправильним Type=notify.

7) Конфіг-менеджмент бореться з вами

Ви «виправляєте» оверрайд вручну, а через 5 хвилин агент управління конфігами повертає попередній стан. Юніт флапає знову і потрапляє під ліміти. Вітайте: ви виявили невидиму руку політики.

Поширені помилки: симптом → корінь → виправлення

Цей розділ має змінити рішення. Якщо впізнаєте симптом — пропустіть самобичування і прямо до виправлення.

Симптом: «Запит на запуск повторювався занадто швидко» після того, як ви встановили Restart=always

Корінь: Ви замаскували реальну помилку агресивним рестартом. Додаток виходить з причин — конфіг, права, залежність, креш.

Виправлення: перемкніть на Restart=on-failure, поставте RestartSec=5s, потім знайдіть першу помилку в journalctl -u. Лише після стабілізації повертайтесь до лімітів.

Симптом: Сервіс стартує вручну, але падає під systemd

Корінь: інше оточення: робоча директорія, PATH, змінні середовища, права користувача або відсутність інтерактивного shell-налаштування.

Виправлення: перегляньте systemctl show -p WorkingDirectory -p Environment, встановіть їх явно і тестуйте з sudo -u.

Симптом: Юніт «таймаутиться», а потім потрапляє під ліміт

Корінь: неправильний Type= (notify без notify) або повільна готовність залежності; systemd вбиває після TimeoutStartSec.

Виправлення: ставте Type=simple, якщо не впевнені, або реалізуйте notify; налаштовуйте TimeoutStartSec тільки після впевненості, що це дійсно повільний старт, а не застрягання.

Симптом: «Failed to start» з exit-code, але корисних логів додатку немає

Корінь: stdout/stderr не доходить до журналу (кастом-логування), або процес падає до ініціалізації логера; іноді ExecStart вказує не на той файл.

Виправлення: перевірте, що ExecStart існує і виконуваний; тимчасово додайте StandardOutput=journal і StandardError=journal в drop-in; перевірте через systemctl cat.

Симптом: Виправлення працює до перезавантаження, потім знову флапає

Корінь: директорії стану в /run не створюються, або відсутні tmpfiles; або порядок залежить від часу завантаження.

Виправлення: використовуйте RuntimeDirectory= або StateDirectory= в юніті; забезпечте права через systemd замість ad-hoc скриптів.

Симптом: Start limit потрапляє тільки під час деплоїв

Корінь: скрипти деплою багаторазово перезапускають під час заміни артефактів/конфігів, викликаючи тимчасові відмови в щільних циклах.

Виправлення: координуйте кроки деплою: зупиніть юніт, замініть артефакти, перевірте конфіг, потім стартуйте один раз. Розгляньте ExecReload де застосовно.

Симптом: Юніт показує «start-limit-hit» навіть після виправлення додатку

Корінь: systemd пам’ятає стан помилки.

Виправлення: виконайте systemctl reset-failed myapp.service, потім запустіть знову. Якщо воно знову падає — ви не виправили.

Три короткі історії з практики

Історія 1: Інцидент через хибне припущення

У них був Debian-API сервіс за балансувальником. Після планового перезавантаження половина пулу повернулась «нездоровою». Панелі сигналізували голосно, але не допомагали: хости були вгорі, мережа виглядала нормально, а systemd викидав «Запит на запуск повторювався занадто швидко», ніби це була типовa справа.

On-call команда припустила класичне: «network-online.target знову ненадійний». Тож у паніці хтось збільшив TimeoutStartSec до кількох хвилин і додав більше After=, послідовно блокуючи завантаження за додатковими таргетами.

Реальна причина була нудною: сервіс працював як User=api, а post-install скрипт пакета перезаписав конфіг з правами тільки для root. Додаток виходив з «permission denied», systemd запускав цикл перезапусків, поки не спрацював ліміт. Ніякого відношення до мережі.

Коли вони перевірили journalctl -u і виконали sudo -u api test -r, все стало очевидно. Вони виправили власність файлу і додали перевірку прав у CI. «Проблема systemd» зникла, бо це ніколи не була проблема systemd.

Також вони видалили панічні залежності завантаження. Це зменшило час завантаження і запобігло подальшим випадковим відмовам. Урок: приписуйте банальні речі першими — права, шляхи, порти — перед тим як створювати міфологію навколо таргетів і порядку.

Історія 2: Оптимізація, що відбилася боком

Платформна команда хотіла швидшого відновлення після тимчасових відмов. Міркування: якщо сервіс падає — рестартуємо негайно. Вони поставили Restart=always і RestartSec=200ms на флоті мікросервісів. Також збільшили StartLimitBurst, бо «systemd не має здаватися».

Декілька тижнів все було гаразд. Потім одна залежність почала повертати пошкоджені відповіді після невдалого релізу. Один клієнт крешував при розборі. З новою політикою він не просто падав — він почав миттєво перезапускатися з кулемета. Кожен рестарт перезавантажував конфіг, відкривав з’єднання, писав логи і підривав кеші CPU. Хости не впали миттєво, але час відгуку підскочив. Причина не в тому, що сервіс був недоступний, а в тому, що він так інтенсивно флапав, що виснажував сусідів.

Вони фактично замінили «крах» на «тривалий denial-of-service проти власних нод». Підвищений StartLimitBurst означав, що systemd довше приєднувався до хаосу. Радіус вибуху розширився від одного клієнта до шумного кластера.

Виправлення було не героїчним. Вони скасували агресивні налаштування рестарту, повернули консервативні ліміти і додали backoff в сам додаток, коли upstream-дані виглядали підозріло. Потім використали Restart=on-failure і RestartSec=5s для більшості сервісів, дозволивши короткі рестарти тільки для дійсно безстанних та добре поводжених демонів.

Одне «оптимізаційне» рішення лишили: коротка валідація конфігу в ExecStartPre, щоб поганий конфіг падав один раз і залишався упавшим. Це саме той швидкий провал, який бажаний — швидко виявлений, але не швидко повторюваний.

Історія 3: Нудна, але правильна практика, що врятувала день

Демон, що відповідав за монтування і перевірку зашифрованих томів, був не найгучнішим, але фундаментальним. Команда реалізувала три непоказні звички: drop-ins у /etc, явні StateDirectory= і RuntimeDirectory=, та короткий health-check з чіткими логами в журнал.

Одного ранку, після оновлення ОС, частина хостів почала провалювати підготовку томів. Залежні сервіси показували «Запит на запуск повторювався занадто швидко», бо необхідний маунт так і не з’явився. Різниця була в тому, що логи підготовчого юніта були чисті і специфічні: відсутній модуль ядра на підмножині хостів.

Оскільки залежності були змодельовані з Requires= та ясним порядком, режим відмови був контрольований. Замість того, щоб вся армада сервісів флапала, вони швидко впали і лишилися вниз. Це звучить погано, поки не бачиш альтернативу: стадо рестартів, що поховає корінь проблеми під шумом.

Виправлення було простим: встановити пакет модуля ядра і перебудувати initramfs на уражених нодах. Відновлення теж було простим: systemctl reset-failed на таргетах і старт. Ніякої дивної правки vendor-юнітів, ніяких «тимчасових» симліків у /lib.

Жарт #2: Найкращий трюк SRE — зробити аварію нудною, бо збудження — це просто даунтайм з кращим маркетингом.

Чеклисти / покроковий план

Покроково: стабілізувати флапаючий юніт без того, щоб приховати проблему

  1. Зупиніть цикл: якщо він осідає вузол, зупиніть його.
    cr0x@server:~$ sudo systemctl stop myapp.service
    

    Рішення: якщо зупинка поліпшує навантаження, ви були в штормі рестартів. Тримайте його зупиненим під час дебагу.

  2. Отримайте реальну помилку: читайте логи за першою відмовою.
    cr0x@server:~$ journalctl -u myapp.service -b --since "30 minutes ago" --no-pager | head -n 60
    Dec 29 09:12:10 server myapp[18934]: ERROR: cannot read config file: /etc/myapp/config.yml: permission denied
    Dec 29 09:12:10 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
    ...

    Рішення: якщо бачите чітку помилку додатку — виправляйте її першою. Якщо логів немає — перевірте ExecStart і тимчасово ввімкніть вивід у журнал.

  3. Підтвердіть ефективну конфігурацію юніта:
    cr0x@server:~$ systemctl show myapp.service -p FragmentPath -p DropInPaths -p User -p Group -p ExecStart -p Type -p Restart -p RestartUSec
    FragmentPath=/lib/systemd/system/myapp.service
    DropInPaths=/etc/systemd/system/myapp.service.d/override.conf
    User=myapp
    Group=myapp
    ExecStart={ path=/usr/local/bin/myapp ; argv[]=/usr/local/bin/myapp --config /etc/myapp/config.yml ; ... }
    Type=simple
    Restart=on-failure
    RestartUSec=250ms

    Рішення: якщо RestartSec занадто малий — виправте його в override. Якщо Type неправильний — виправте. Якщо User/Group не відповідає власності файлів — відкоригуйте права або юніт.

  4. Застосуйте довговічний оверрайд через drop-in:
    cr0x@server:~$ sudo systemctl edit myapp.service
    

    Використовуйте налаштування на кшталт RestartSec=5s і консервативні ліміти запуску поки стабілізуєтеся.

  5. Перезавантажте systemd і скиньте стан помилки:
    cr0x@server:~$ sudo systemctl daemon-reload
    cr0x@server:~$ sudo systemctl reset-failed myapp.service
    

    Рішення: якщо ви забули daemon-reload — ваші правки не застосуються. Якщо забули reset-failed — systemd може і надалі відмовлятися стартувати.

  6. Запустіть один раз, спостерігайте:
    cr0x@server:~$ sudo systemctl start myapp.service
    cr0x@server:~$ systemctl status myapp.service --no-pager
    ● myapp.service - MyApp API
         Active: active (running) since Mon 2025-12-29 09:30:01 UTC; 2s ago
    

    Рішення: якщо він знову падає, не продовжуйте сліпо перезапускати. Поверніться до логів; ітеруйте свідомо.

Чеклист: «Чи заслуговує цей юніт на перезапуск взагалі?»

  • Якщо це пакетне завдання: використовуйте Type=oneshot, розгляньте Restart=no і дозволяйте голосну помилку.
  • Якщо це демон, що має бути завжди присутній: зазвичай підходить Restart=on-failure.
  • Якщо він виходить чисто в рамках нормальної роботи: уникайте Restart=always, інакше створите цикл спеціально.
  • Якщо залежить від мережі чи upstream: віддавайте перевагу retry/backoff на боці додатку, поки процес залишається запущеним.

Чеклист: безпечні значення за замовчуванням для більшості внутрішніх сервісів

  • Restart=on-failure
  • RestartSec=5s (2s для дуже легких stateless-демонів; 10s для важких)
  • StartLimitIntervalSec=60s
  • StartLimitBurst=3 (5 теж підходить; оберіть так, щоб це вимагало уваги людини)
  • Явний User=, Group=
  • WorkingDirectory= якщо додаток використовує відносні шляхи
  • RuntimeDirectory=/StateDirectory= для директорій, які потрібні сервісу

Цікаві факти та історія (видання про обмеження запуску systemd)

  • Systemd не винайшов цикли рестартів; він просто зробив їх легшими для вираження через Restart= і більш помітними через стан юніта.
  • «Start limit» — на рівні юніта, не глобальний. Один флапаючий сервіс можна ізолювати, не караючи інші — якщо він не виснажує всю машину.
  • Каталоги drop-in (/etc/systemd/system/UNIT.d/*.conf) існують спеціально для того, щоб оновлення пакетів не переписували локальні операційні наміри.
  • Розділення Debian між /lib і /etc — свідомий вибір: vendor-файли в /lib, адмінські зміни в /etc.
  • Журнал systemd задуманий для захоплення структурованої метаданих (ім’я юніта, PID, cgroup), щоб ви могли відповісти «що сталося?» без археології grep.
  • Обмеження запуску — функція безпеки, що запобігає лавині логів і перевантаженню CPU; фактично це автоматичний рубіж для супервізії процесів.
  • Type=notify з’явився, щоб замінити «sleep 5 і надіятися на краще» готовністю з явним сигналом — прекрасно, якщо використовується правильно, караюче, якщо імітується.
  • network-online.target часто неправильно розуміють: це не «мережа існує», а «компонент заявляє, що мережа налаштована», що може бути повільно або невірно залежно від стеку.
  • Скидання failed-юнітів — явна дія оператора, бо systemd трактує повторну відмову як значущий стан, а не як тимчасову примху.

Поширені питання (FAQ)

1) Чи означає «Запит на запуск повторювався занадто швидко», що systemd зламався?

Ні. Це означає, що ваш сервіс кілька разів впав і systemd застосував налаштований ліміт. Сервіс або неправильно описаний, або зламаний, а systemd просто запобігає нескінченному шуму.

2) Як очистити ліміт запуску, щоб спробувати знову?

Після внесення реального виправлення виконайте:

cr0x@server:~$ sudo systemctl reset-failed myapp.service
cr0x@server:~$ sudo systemctl start myapp.service

Якщо він одразу знову потрапляє під ліміт — ви не виправили кореневу причину.

3) Чи варто збільшувати StartLimitBurst, щоб зробити систему «стійкішою»?

Зазвичай — ні. Збільшення burst ховає реальні відмови і підвищує шум на хості. Краще виправити реальну помилку і використовувати розумний RestartSec. Якщо й збільшуєте burst — робіть це помірно і навмисно.

4) У чому різниця між RestartSec і StartLimitIntervalSec?

RestartSec — затримка між спробами перезапуску. StartLimitIntervalSec — вікно, в межах якого systemd рахує невдалі запуски, а StartLimitBurst — максимум дозволених спроб у цьому вікні.

5) Чому це падає тільки при завантаженні, але працює коли я запускаю пізніше?

Часи завантаження виявляють проблеми залежностей: відсутні маунти, недоступна мережа, БД не готова, не створені runtime-директорії. Замоделюйте залежність або зробіть додаток толерантним, і уникайте надмірного використання network-online.target.

6) Я відредагував юніт, але нічого не змінилось. Чому?

Поширені причини: ви редагували vendor-юніт у /lib і він був перезаписаний; або забули systemctl daemon-reload; або налаштування містило опечатку і було проігнороване. Використайте systemd-analyze verify і systemctl show, щоб підтвердити ефективні значення.

7) Чи нормально ставити Restart=always для критичних сервісів?

Іноді так. Але тільки якщо ви розумієте поведінку виходу сервісу і впевнені, що його безпечно перезапускати завжди. Багато сервісів цілеспрямовано виходять під час оновлень чи змін конфігу; Restart=always може боротися з цими робочими процесами.

8) Як визначити, чи таймер або щось інше тригерить запуск?

Перевірте таймери і зворотні залежності:

cr0x@server:~$ systemctl list-timers --all
cr0x@server:~$ systemctl list-dependencies --reverse myapp.service

Якщо таймер спрацьовує щохвилини — імовірно, ви бачите повторні спроби, які не йдуть від Restart=.

9) Коли варто змінювати TimeoutStartSec?

Тільки після підтвердження, що сервіс справді довго стає готовим. Якщо додаток завершується миттєво — збільшення таймауту нічого не дасть. Якщо Type=notify вказано неправильно — ви лікуєте не ту проблему.

10) Можу я змусити systemd логувати більше про те, чому він перестав спроби?

Systemd уже логгить подію start-limit. Частіше бракує логів самого додатку. Переконайтеся, що він пише у stderr/stdout або налаштуйте логування так, щоб journalctl -u захоплював першу причину відмови.

Висновок: кроки, які тримають вас подалі від канави

«Запит на запуск повторювався занадто швидко» — це доброта. Systemd каже вам, що сервіс замикається в щільному циклі помилок, і він не дозволить вам спалити хост. Ставтеся до цього як до контрольного пункту, а не кореня проблеми.

Наступні кроки, які дійсно працюють у середовищах Debian 13:

  • Знайдіть першу реальну помилку за допомогою journalctl -u UNIT і не читайте тільки останній рядок.
  • Перевірте ефективну конфігурацію за допомогою systemctl show і systemctl cat. Довіряйте тому, що systemd запускає, а не тому, що, як вам здається, ви написали.
  • Виправте проблему виконання (права, порти, залежності, робоча директорія) перед тим, як чіпати ліміти.
  • Застосовуйте довговічні зміни через drop-ins у /etc/systemd/system/UNIT.d/, потім daemon-reload.
  • Встановіть розумну семантику перезапуску: Restart=on-failure, адекватний RestartSec, консервативні StartLimit*.
  • Скидайте стан failed лише після змін: systemctl reset-failed.

Якщо зробите хоча б одну річ: перестаньте ставитись до systemd як до ігрового автомату. Менше тягніть важіль. Більше читайте логи. Сервіс або запуститься — або зламається з причиною, котру ви зможете виправити.

← Попередня
Покинуті контейнери Docker: чому вони з’являються і як безпечно очищати
Наступна →
WireGuard на Windows не підключається: 10 виправлень, що вирішують більшість випадків

Залишити коментар