Чому systemd не перезапустив ваш сервіс
Restart= відстежує лише головний процес unit: завершення дочірнього процесу в тій самій cgroup не запускає перезапуск. Пояснюємо Type=, ліміти й journal.
Коротка відповідь: політики перезапуску systemd відстежують один процес
Політики перезапуску systemd відстежують один процес на unit: головний процес. Restart= читає статус завершення лише цього процесу. До інших процесів він не звертається. У control group unit може бути двадцять процесів. Один із них може завершитися, а unit залишиться active (running), оскільки головний процес усе ще працює. З погляду systemd помилки немає, тому перезапуск не виконується.
systemd знає про інші процеси. Він завершує їх, коли зупиняє unit, враховує їхнє споживання пам’яті в лімітах unit, застосовує до них квоту CPU unit і показує їх у systemctl status. Але статус завершення цих процесів він не перевіряє. Логіка перезапуску та cgroup — це різні механізми. Більша частина цього посібника присвячена розриву між ними.
Що містить cgroup і які дані читає логіка перезапуску
cgroup (control group) — це об’єкт ядра, який містить набір процесів. Кожен service unit отримує власну cgroup з іменем цього unit. Процес не може вийти з неї. Дочірні процеси успадковують cgroup батьківського процесу, а непривілейований процес не може перемістити себе до іншої cgroup. Тому systemd може очистити процеси daemon, який двічі виконує fork. Старі init scripts не могли робити це надійно.
Порівняйте обидва факти:
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.servicesystemd-cgls перелічує всі процеси в unit. MainPID — це єдине число, яке читає політика перезапуску. Якщо ці два значення не відповідають вашій моделі, помилка саме в цьому. MainPID=0 гірше за неправильний PID: це означає, що systemd взагалі нічого не відстежує, тому жодне значення Restart= не зможе спрацювати.
Існує один реальний виняток із правила головного процесу. Якщо kernel out-of-memory killer завершує будь-який процес усередині cgroup unit, systemd це бачить, оскільки відстежує файл memory.events цієї cgroup. OOMPolicy= визначає подальшу дію. Типове значення — stop: весь unit зупиняється, результат записується як 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'.Отже, дочірній процес, завершений через нестачу пам’яті, справді зупиняє unit, тоді як завершення того самого процесу через segmentation fault — ні. Якщо ви встановлюєте обмеження пам’яті для unit, перед налаштуванням політики перезапуску прочитайте як MemoryMax і CPUQuota застосовуються до cgroup unit, оскільки саме тут ці дві функції взаємодіють.
Як Type= визначає головний процес
Type= у розділі [Service] визначає не лише порядок запуску. Це правило визначає, який PID (ідентифікатор процесу) стане MainPID. Від цього також залежить, що саме зможе бачити Restart=.
Type=simple— значення за замовчуванням. Процес, який systemd створює через fork ізExecStart=, стає головним процесом. systemd одразу позначає unit як запущений, ще до того, як визначить, чи справді спрацювавexec. Помилка в шляху до бінарного файла дає успішне завдання запуску, а потімMain process exited, code=exited, status=203/EXECчерез мить.Type=execпрацює якsimple, але завдання запуску очікує успішного завершенняexec. Тому наведена вище помилка перетворюється на коректно зафіксовану помилку запуску. Потрібен systemd 240 або новіший, який є в кожному підтримуваному дистрибутиві. Використовуйте його замістьsimple.Type=forkingочікує, що процес ізExecStart=створить фоновий daemon, а потім завершиться. systemd очікує завершення батьківського процесу, а потім шукає фактичний daemon. ВкажітьPIDFile=. Без ньогоGuessMainPID=(увімкнене за замовчуванням) працює лише тоді, коли в cgroup залишається рівно один процес. Якщо залишити два процеси,MainPIDзалишається0.Type=notifyозначає, що сервіс викликаєsd_notify(3)і надсилаєREADY=1, коли готовий обробляти мережевий трафік. Він також може надіслатиMAINPID=, щоб передати systemd інший процес для відстеження.NotifyAccess=за замовчуванням має значенняmain, тому сповіщення від дочірнього процесу ігнорується, а в журналі зазначається PID процесу, від якого воно надійшло.Type=oneshotне має постійного головного процесу. Unit переходить у неактивний стан одразу після завершення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, оскільки кожна з них залишає unit у стані, який без очевидної причини виглядає зламаним:
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.Перша означає, що daemon записує свій PID-файл в інше місце або робить це пізніше, ніж systemd перевіряє файл. Друга означає, що PID-файл містить PID процесу за межами cgroup unit. systemd відмовляється приймати такий процес, оскільки доступний для запису PID-файл інакше міг би стати способом змусити systemd надсилати сигнали будь-якому процесу на сервері.
Чому wrapper script приховує завершення дочірніх процесів
Ось схема, яка породжує запитання із заголовка.
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
waitUnit — це Type=simple, тому головний процес — shell. wait без аргументів повертається лише після завершення всіх дочірніх процесів. Завершіть worker, і shell продовжить чекати на web process. Тому shell не завершується, MainPID не завершується, а Restart= так і не перевіряється. Тепер cgroup містить на один процес менше, systemctl status виводить коротше дерево, а unit і далі має стан active (running). systemd не відстежує зміни в цьому дереві.
Другий варіант тієї самої помилки менш помітний:
ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'Головний процес — shell, а не myapp. Під час systemctl stop systemd надсилає SIGTERM головному процесу, але shell, який очікує на foreground child, не передає цей сигнал далі. Тоді зупинка триває всі 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, і shell буде замінено програмою. Тоді MainPID — це програма, і сигнали надходять безпосередньо до неї. Ще краще — видаліть shell і використайте в unit Environment= або EnvironmentFile=. Зверніть увагу: ця помилка непомітна, коли рядок -c містить одну команду, оскільки bash і dash оптимізують такий випадок до прямого exec. Додайте до рядка другу команду — і shell залишиться запущеним перед вашою програмою.
Відтворіть проблему на тестовому VPS за дві хвилини
Збережіть наведений вище wrapper як /usr/local/bin/two-children.sh, зробіть його виконуваним за допомогою chmod +x і замініть два шляхи до програм на sleep 3600. Вкажіть цей wrapper в unit за допомогою Type=simple і Restart=on-failure, потім виконайте systemctl daemon-reload та запустіть unit. Виконайте systemd-cgls --unit two-children.service і зверніть увагу на три PID: shell і два його дочірні процеси. Завершіть один дочірній процес за допомогою sudo kill <pid>. Перевірте unit ще раз. Дерево стало на один процес коротшим, стан і далі active (running), а в журналі немає нових повідомлень. Тепер виконайте sudo kill -9 <shell pid>. Unit завершується помилкою, дочірній процес, що залишився, буде очищено, оскільки 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— значення за замовчуванням. Unit ніколи не перезапускає себе. Тому unit без рядкаRestart=завершується після першого збою і залишається неактивним.on-successперезапускає unit лише після коректного завершення.on-failureперезапускає unit після ненульового коду виходу, некоректного завершення сигналом, перевищення тайм-ауту запуску або зупинки чи спрацювання watchdog.on-abnormalперезапускає unit після некоректного завершення сигналом, перевищення тайм-ауту або спрацювання watchdog, але не після звичайного ненульового коду виходу.on-abortперезапускає unit лише після некоректного завершення сигналом, тобто після збою.on-watchdogперезапускає unit лише після завершення терміну діїWatchdogSec=.alwaysперезапускає unit після кожного з перелічених випадків, зокрема після коректного завершення зі статусом 0.
on-failure — правильне значення за замовчуванням для довготривалого daemon. Воно відновлює daemon після збою, але не запускає його знову після навмисного exit 0. always підходить програмі, яка завершується коректно з причин, що не залежать від неї, наприклад tunnel client, який повертає 0 після розриву з’єднання віддаленою стороною. Недолік always полягає в тому, що це приховує помилки: сервіс може запуститися, прочитати пошкоджений конфігураційний файл, записати помилку в журнал і завершитися з кодом 0. Тоді він перезапускатиметься без кінця, а єдиною ознакою буде зростання лічильника перезапусків.
SuccessExitStatus= змінює межу між коректним і некоректним завершенням. Borg завершується з кодом 1 для попереджень і з кодом 2 для помилок. Тому backup unit без SuccessExitStatus=1 щоразу позначається як failed, якщо пропускає один файл, який неможливо прочитати. RestartPreventExitStatus= містить коди, які блокують перезапуск навіть за використання always. Це коректний спосіб повідомити, що програму не потрібно запускати знову. RestartForceExitStatus= робить протилежне. Для backup job слід використовувати Type=oneshot unit, яким керує timer, а не цикл перезапуску. Структура пари service і timer, яка запускає job за розкладом підходить як приклад.
Є важливе застереження щодо тестування. Якщо завершити сервіс звичайною командою kill <pid>, вона надсилає сигнал SIGTERM, який входить до переліку коректних сигналів. Тому Restart=on-failure правильно нічого не робить, і можна помилково вирішити, що конфігурація несправна. Використовуйте натомість kill -9 <pid> або systemctl kill -s SIGKILL myapp.service. Також пам’ятайте, що жодне значення Restart= не спрацьовує після systemctl stop або коли unit було зупинено через зникнення залежності BindsTo= чи PartOf=. Задача зупинки не є помилкою.
RestartSec і стандартне значення 100 мілісекунд
RestartSec= — це пауза між зупиненням unit і повторним запуском systemd. Стандартне значення становить 100 мілісекунд. Перевірте, яке значення фактично завантажив ваш unit:
systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.serviceДля unit, у якому це значення не задано, буде виведено RestartUSec=100ms. Це стандартне значення підходить для сервісу, який один раз аварійно завершується, а потім запускається знову. Воно не підходить для сервісу, який взагалі не може запуститися, оскільки п’ять спроб перезапуску відбуваються менш ніж за пів секунди. Саме це активує описане далі обмеження частоти перезапусків. Для сервісів, які очікують на базу даних, змонтований диск або мережевий маршрут, задайте RestartSec=5s або більше.
Станом на August 2026 systemd 254 і новіші версії також підтримують RestartSteps= і RestartMaxDelaySec=. Вони поступово збільшують затримку від RestartSec= до граничного значення протягом зазначеної кількості спроб. Ubuntu 24.04 постачається з systemd 255 і підтримує ці параметри. Debian 12 постачається з systemd 252 і не підтримує їх. Збільшення затримки є правильним рішенням, коли залежний компонент може бути недоступним тривалий час.
Що насправді означає «start request repeated too quickly»
Через це повідомлення користувачі часто думають, що systemd довільно припиняє спроби запуску. Насправді це лічильник. Правило таке: якщо unit запускається понад StartLimitBurst= разів протягом StartLimitIntervalSec=, systemd відмовляється запускати його знову та переводить його у failed state. Типові значення за замовчуванням — 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 скидає лічильник і failed state. Інший спосіб цього не робить, тому звичайний 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У цьому прикладі unit має п’ять спроб протягом п’ятихвилинного інтервалу, після чого systemd припиняє спроби запуску. StartLimitIntervalSec=0 повністю вимикає ліміт. Пам’ятайте про наслідки: сервіс, який не може запуститися, тепер повторюватиме спроби без кінця й щоразу записуватиме повідомлення в журнал. Загальносистемні значення за замовчуванням зберігаються в /etc/systemd/system.conf як DefaultStartLimitIntervalSec= і DefaultStartLimitBurst=.
Окремо зверніть увагу на сусідній параметр. StartLimitAction= визначає поведінку після досягнення ліміту та приймає, зокрема, значення reboot, reboot-force і poweroff. Значення за замовчуванням — none. У цьому режимі unit переходить у failed state, а система продовжує працювати. На віддаленому VPS значення poweroff означає, що сервер залишиться вимкненим, доки ви не відкриєте консоль у панелі провайдера.
Виправлення 1: один процес на unit
У більшості випадків це правильне рішення. Якщо потрібно запускати дві програми, створіть два unit-файли. Кожен із них матиме власний основний процес, фактичний код завершення та окрему політику перезапуску. Ви також отримаєте окремі журнали, ліміти ресурсів і лічильники перезапусків. Саме це потрібно, коли проблема виникає о третій годині ночі.
Описуйте зв’язок між unit-файлами в самих unit-файлах, а не в shell-скрипті.
After=визначає лише порядок запуску. Ця директива нічого не повідомляє про збої.Requires=запускає інший unit одночасно з цим і зупиняє цей unit, якщо інший зупинено явно.BindsTo=— цеRequires=плюс потрібна вам поведінка: цей unit зупиняється, коли інший зупиняється з будь-якої причини, зокрема через аварійне завершення. Додайте такожAfter=, інакше порядок запуску не визначений.PartOf=поширює зупинку та перезапуск на залежні unit-файли, томуsystemctl restart myapp.targetдосягає кожного unit-файлу, який є для ньогоPartOf=.Upholds=(у systemd 249 і новіших версіях, тобто в Ubuntu 22.04 і новіших) підтримує вказаний unit у запущеному стані: якщо він зупиняється, systemd запускає його знову. На нього поширюється той самий ліміт частоти запуску, що й на всі інші unit-файли.
Worker, який не повинен працювати без свого 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=5sWorker не має секції [Install], і його ніколи не вмикають вручну. API unit підтягує його через 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.servicesystemd-analyze verify нічого не виводить, якщо файл коректний. Будь-який вивід означає проблему. Зазвичай це ключ systemd не розпізнає в секції, де його вказано, або залежність від unit-файлу, якого не існує.
Виправлення друге: Type=notify, щоб systemd знав більше, ніж лише PID
Якщо програма підтримує протокол сповіщень systemd, використовуйте його. За допомогою Type=notify сервіс повідомляє systemd, коли він готовий. Це забезпечує фактичне впорядкування запуску, а не лише припущення. Також сервіс може надсилати MAINPID=, щоб указати systemd на потрібний процес, а не на процес запуску.
WatchdogSec= вартий докладених зусиль. Установіть його, і сервіс буде зобов’язаний надсилати WATCHDOG=1 через sd_notify(3) принаймні з такою частотою. Якщо повідомлення припиняють надходити, systemd завершує сервіс сигналом SIGABRT і позначає його як невдалий. Тоді 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)!, після чого відбувається завершення процесу. Якщо натомість unit залишається у стані activating (start), доки не спливе TimeoutStartSec, повідомлення READY=1 не надійшло. Або програма не підтримує цей протокол, або NotifyAccess=main відхиляє сповіщення, яке надійшло від дочірнього процесу. Журнал зазначає обидва PID.
Для програм, які надають HTTP endpoint для перевірки стану, але не підтримують sd_notify, є два коректні варіанти: створити невеликий timer unit, який перевіряє endpoint і викликає systemctl restart, або доручити перевірку container runtime. Саме для цього призначені healthcheck у Compose та їхня поведінка під час перезапуску.
Виправлення 3: супервізор усередині unit, лише якщо іншого вибору немає
Деяке програмне забезпечення справді постачається як набір процесів, якими керує launcher, що його неможливо розділити. У такому разі запустіть супервізор усередині unit і прийміть наслідок: systemd стежить за супервізором, супервізор — за всіма іншими процесами, а політика перезапуску тепер зберігається у двох файлах.
Типовий приклад — container runtime. Unit docker compose або podman є саме таким варіантом: політика перезапуску окремих контейнерів задається у Compose-файлі, а unit systemd лише підтримує роботу runtime. Якщо у вас така схема, у статті описано unit, який запускає Compose stack під час завантаження системи робочий варіант, зокрема пояснено, чому Type=oneshot разом із RemainAfterExit=yes зазвичай є правильним вибором.
cgroup і далі працює на вашу користь. Усе, що запускає супервізор, залишається в cgroup цього unit, тому MemoryMax=, CPUQuota= і очищення під час зупинки й надалі охоплюють усе дерево процесів. Делеговано лише рішення про перезапуск.
Незалежно від вибраного супервізора, не встановлюйте Restart=always для зовнішнього unit і агресивну політику перезапуску всередині нього без попереднього аналізу. Два рівні логіки перезапуску з власними затримками призводять до того, що сервіс кілька хвилин безперервно перезапускається, а журнал не пояснює причину.
ExitType=cgroup не означає «перезапускати, коли завершується будь-який процес»
ExitType= (у systemd 250 і новіших версіях, тому він є і в Ubuntu 24.04, і в Debian 12) — це параметр, який знаходять під час пошуку розв’язання цієї проблеми. Він працює протилежно до того, що можна припустити з його назви. Значення за замовчуванням, ExitType=main, означає, що сервіс вважається зупиненим після завершення головного процесу. Значення ExitType=cgroup означає, що сервіс вважається запущеним, доки не завершиться останній процес у cgroup.
Отже, ExitType=cgroup зменшує чутливість unit до завершення одного процесу, а не збільшує її. Це правильний параметр для програми, яка створює окремий worker-процес, завершує батьківський процес і не записує PID-файл, через що Type=forking не може знайти daemon. Для описаного тут випадку це неправильний параметр.
Значення Restart=, яке означало б «перезапускати unit, коли завершується будь-який процес у cgroup», не існує. Якщо потрібна така поведінка, кожен процес має працювати в окремому unit. Якщо розділити програму неможливо, але ви керуєте wrapper script, найближчий варіант — wait -n. Він повертає керування одразу після завершення першого дочірнього процесу:
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1Тепер завершення будь-якого дочірнього процесу завершує wrapper із ненульовим статусом, тому спрацьовує 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-precisesystemctl status показує стан, основний PID і дерево cgroup на одному екрані. Справний unit має стан Active: active (running) і рядок Main PID: із назвою очікуваного процесу. Якщо внизу дерева є незнайомі процеси або немає процесу, який має бути присутнім, відповідь уже знайдено.
systemd-cgls --unit виводить те саме дерево без обрізання. Це важливо, коли 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-файл. coredumpctl list покаже його, якщо встановлено systemd-coredump.
Якщо йдеться про кілька машин, NRestarts — це показник, який варто збирати за розкладом. Якщо лічильник unit зростає щодня, unit щодня завершується з помилкою, навіть якщо цього ніхто не помітив. Коли машин уже більше двох або трьох, уніфікований спосіб виконати одну команду на кожному сервері перетворює припущення на звіт.
FAQ
Чому systemctl повідомляє, що мій сервіс активний, хоча процес завершився?
systemd відстежує один процес на unit сервісу — головний процес, а Restart= читає лише код завершення цього процесу. Усі інші процеси, запущені unit, перебувають у тій самій cgroup. systemd завершить їх, коли unit зупиниться, але не відстежує їхнє завершення. Виконайте systemctl show -p MainPID myapp.service і порівняйте це число з systemd-cgls --unit myapp.service. Якщо завершений процес є в дереві, але не є MainPID, systemd працює саме так, як передбачено. Виправлення полягає в тому, щоб використовувати один процес на unit і задекларувати зв’язок між unit за допомогою BindsTo= та Upholds=.
Що означає повідомлення "start request repeated too quickly"?
Це означає, що unit запускали понад 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 взагалі не розпізнає, і не виводить нічого, якщо файл не містить помилок.