Ubuntu 24.04 «Failed to start …»: найшвидший робочий процес діагностики systemd (випадок №2)

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

Ви перезавантажуєте вузол у продакшені, і systemd вітає вас найменш інформативним рядком в історії Linux:
«Failed to start …». Ніякого контексту. Жодної підказки, який шар збрехав. Лише сумний імʼя юніта й відмітка часу, що вже в минулому.

Це випадок №2: не демонстрація та не ситуація «просто перевстановіть». Це відтворюваний робочий процес, який я використовую, коли служба
не запускається на Ubuntu 24.04 і потрібна швидка відповідь: що зламалося, де це сталося і яке рішення приймати далі.
Швидкість важлива, але коректність важливіша — бо другий перезавантаження і є початок справжнього простою.

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

Коли юніт не вдається запустити, ви не «налагоджуєте сервіс». Ви налагоджуєте транзакцію, яку systemd намагався виконати:
залежності, порядок, середовище, привілеї, файлову систему, мережу та сам бінарник.
Найшвидший шлях — припинити гадати і змусити systemd сказати, на якій фазі сталося лушпіння.

1) Підтвердіть точну назву юніта й режим помилки

  • Отримайте назву юніта, а не «імʼя програми».
  • Отримайте результат і крок (EXEC, SIGNAL, TIMEOUT тощо).
  • Отримайте останні логи лише для цього юніта.

2) Визначте вузьке місце шару

Вузьке місце майже завжди одне з цих:
ExecStart не може виконатися (відсутній бінарник, дозволи, SELinux/AppArmor),
процес стартував, але вийшов (налаштування, порти, секрети),
він не став «готовим» (Type=notify, перевірка готовності, PIDFile),
або чекає на залежності (network-online, mount, БД, віддалене сховище).

3) Вирішіть, чи потрібне безпечне для перезавантаження виправлення або аварійний обхід

Є два види виправлень:
правильні (override юніта, виправлення залежностей, виправлення конфігу),
і тріаж (тимчасове маскування, зниження залежностей, ручний запуск).
Якщо хост застряг під час завантаження, аварійний обхід допустимий — але задокументуйте його і видаліть пізніше.

4) Якщо сумніваєтесь, слідуйте по ланцюжку, а не за симптомом

Сервіс, що не стартує, часто не зламався. Він чекає на монтування, DNS або маршрут, який ніколи не зʼявиться.
Тож ставтеся до нього як до розподіленої системи: знайдіть найранішу невідповідність у ланцюгу.

Цитата, яку я досі використовую в постмортемах, від W. Edwards Deming: «Погана система переможе хорошу людину кожного разу.»
Якщо ви дивитесь на «Failed to start», припускайте, що система (залежності, порядок, середовище) винна, поки не доведете протилежне.

Дев’ять фактів, які змінюють підхід до налагодження systemd

  1. systemd давно замінив Upstart в Ubuntu, але суттєвий злам — це не «новий init», а планування через граф залежностей. Тепер налагодження — це обхід графа.
  2. journald — це не «просто логи»; він зберігає структуровані поля (unit, PID, cgroup, шлях виконуваного). Ви можете вирізати логи точно, без grep-спагеті.
  3. «Failed to start» — це підсумок інтерфейсу, а не корінна причина. Справжня причина зазвичай у меншому повідомленні: «failed at step EXEC» або «start request repeated too quickly».
  4. Юніти мають дві ортогональні відносини: порядок (After=) і вимога (Requires=/Wants=). Багато відмов трапляється через їх плутанину.
  5. Таргети — це точки синхронізації, а не «runlevel під іншою назвою». Налагодження проблем завантаження часто означає розуміння, який target підтягнув проблемний юніт.
  6. Type=notify поширений у сучасних демонів. Якщо сервіс не надсилає готовність, systemd може оголосити таймаут, хоча процес живий.
  7. StartLimitBurst/Interval — це запобіжники. Сервіс у циклі перезапусків може «впасти», навіть якщо остання спроба була б успішною.
  8. Drop-in override — це першокласна практика. Вендори постачають юніти; оператори їх перекривають. Редагування файлів у /lib/systemd/system створює проблеми для майбутнього вас.
  9. systemd-run і трансієнтні юніти існують. Коли потрібно відтворити проблеми середовища, трансієнтний юніт може відтворити cgroup і обмеження краще, ніж ваша shell-сесія.

Жарт №1: systemd не ненавидить вас особисто. Він ненавидить усіх однаково — а потім записує це в журнал з мілісекундною точністю.

Ментальна модель: звідки насправді береться «Failed to start»

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

Що означає «start» (в термінах systemd)

«Запуск» сервісу включає:

  • Завантаження юніта (парсинг файлу юніта, drop-in, вихід генераторів)
  • Вирішення залежностей (що повинно існувати перед запуском цього юніта)
  • Виконання (ExecStartPre, ExecStart, можливе кілька процесів)
  • Готовність (Type=simple — негайно; Type=notify — чекає сигналу; Type=forking — потребує PID)
  • Моніторинг (політики Restart=, watchdog, відмови)

Поширені класи відмов (використовуйте як відра)

  • Юніт не може виконатися: відсутній бінарник, неправильні дозволи, неправильний користувач, відсутній інтерпретатор, неправильний робочий каталог.
  • Процес завершується з кодом ≠ 0: помилки парсингу конфігу, порт вже зайнятий, залежність недоступна.
  • Таймаут: не повідомлена готовність, зависання в мережі, зависання на монтуванні, повільний диск, брак ентропії (рідко).
  • Дедлок залежностей: неправильний порядок, невірний After=, цикл залежностей.
  • Політика блокує: відмови AppArmor, sandboxing systemd (ProtectSystem, PrivateTmp), обмеження capability.
  • Обмеження частоти: StartLimitHit, цикли перезапусків.

Ваше завдання у тріажі — віднести «Failed to start» до одного з цих відер за менш ніж пʼять хвилин.
Усе інше — інженерія, а не гадання.

Практичні завдання для тріажу (команди + значення + рішення)

Нижче наведені завдання, які я фактично виконую. Кожне включає: команду, що означає вивід, і рішення, яке потрібно прийняти.
Я припускаю, що невдалий юніт називається acme-api.service. Підставляйте свій.

Завдання 1: Підтвердіть стан, результат і останнє коротке повідомлення про помилку

cr0x@server:~$ systemctl status acme-api.service --no-pager
● acme-api.service - Acme API
     Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
     Active: failed (Result: timeout) since Mon 2025-12-30 10:12:48 UTC; 32s ago
   Duration: 1min 30.012s
    Process: 1842 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=killed, signal=TERM)
   Main PID: 1842 (code=killed, signal=TERM)
        CPU: 1.012s

Dec 30 10:11:18 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: acme-api.service: Failed with result 'timeout'.
Dec 30 10:12:48 server systemd[1]: Failed to start acme-api.service - Acme API.

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

Рішення: Далі: зʼясувати, чи це проблема готовності/Type, чи процес чекав щось (мережа, монтування, БД).

Завдання 2: Витягніть логи, привʼязані до юніта (і не гортайте зайвого)

cr0x@server:~$ journalctl -u acme-api.service -b --no-pager -n 200
Dec 30 10:11:18 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 10:11:19 server acme-api[1842]: loading config from /etc/acme/api.yaml
Dec 30 10:11:19 server acme-api[1842]: connecting to postgres at 10.20.0.15:5432
Dec 30 10:11:49 server acme-api[1842]: still waiting for postgres...
Dec 30 10:12:19 server acme-api[1842]: still waiting for postgres...
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: acme-api.service: Failed with result 'timeout'.

Значення: додаток заблокований на Postgres, і таймаут systemd — це вісник проблеми.

Рішення: Дослідіть доступність мережі/DNS/маршрути, і перевірте ланцюг залежностей сервісу (чи потрібен нам After=network-online.target? чи ні).

Завдання 3: Прочитайте файл юніта, який systemd фактично використовує

cr0x@server:~$ systemctl cat acme-api.service
# /etc/systemd/system/acme-api.service
[Unit]
Description=Acme API
After=network.target
Wants=network.target

[Service]
Type=notify
ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml
User=acme
Group=acme
Restart=on-failure
TimeoutStartSec=90

[Install]
WantedBy=multi-user.target

Значення: встановлено Type=notify, отже systemd очікує сигнал готовності. Якщо демон його не надсилає, systemd завершить роботу через таймаут, хоча процес може бути «нормальний».
Також він чекає лише на network.target, що не означає «мережа піднята».

Рішення: Підтвердьте, чи бінарник підтримує sd_notify. Якщо ні — змініть на Type=simple або виправте демон. Також оцініть, чи підходить network-online.target (часто ні).

Завдання 4: Перевірте, чи systemd чекає на залежності замість вашого сервісу

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

Значення: його ніхто несподівано не підтягнув. Але це не показує затримок через порядок; воно показує, хто його хоче.

Рішення: Використайте critical chain, щоб побачити, що блокувало завантаження, і перевірте блоки network-online/mount, якщо вони присутні.

Завдання 5: Використайте critical chain, щоб знайти перший повільний або зламаний юніт

cr0x@server:~$ systemd-analyze critical-chain acme-api.service
acme-api.service +1min 30.012s
└─network.target @8.412s
  └─systemd-networkd.service @6.901s +1.201s
    └─systemd-udevd.service @3.112s +3.654s
      └─systemd-tmpfiles-setup-dev-early.service @2.811s +201ms
        └─kmod-static-nodes.service @2.603s +155ms

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

Рішення: Підтвердіть, чи systemd коли-небудь отримав READY=1, або чи додаток просто чекав Postgres.

Завдання 6: Підтвердіть, чи процес був живий під час «таймауту»

cr0x@server:~$ systemctl show acme-api.service -p MainPID -p ExecMainStatus -p ExecMainCode -p TimeoutStartUSec -p Type
MainPID=0
ExecMainStatus=0
ExecMainCode=0
TimeoutStartUSec=1min 30s
Type=notify

Значення: MainPID зараз 0, бо процес мертвий (systemd його вбив). Ключова лінія — Type=notify і скінченний таймаут.

Рішення: Або демон ніколи не повідомив про готовність, або він не досяг готовності, бо чекав Postgres. Виправте залежність або змініть поведінку старту.

Завдання 7: Перевірте StartLimitHit (сценарій «він впав, бо він впав»)

cr0x@server:~$ systemctl status acme-api.service --no-pager | sed -n '1,18p'
● acme-api.service - Acme API
     Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
     Active: failed (Result: start-limit-hit) since Mon 2025-12-30 10:13:30 UTC; 5s ago
    Process: 1912 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=exited, status=1/FAILURE)

Значення: systemd припинив спроби, бо перезапусків було занадто багато.

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

Завдання 8: Скиньте start-limit і навмисно повторіть спробу

cr0x@server:~$ sudo systemctl reset-failed acme-api.service
cr0x@server:~$ sudo systemctl start acme-api.service
cr0x@server:~$ systemctl status acme-api.service --no-pager -n 20
● acme-api.service - Acme API
     Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
     Active: activating (start) since Mon 2025-12-30 10:14:01 UTC; 3s ago

Значення: він знову активується — тепер дивіться логи.

Рішення: Якщо повторюється, зупиніться і виправте корінну причину (конфіг, залежність, готовність), не продовжуйте підганяти start.

Завдання 9: Перевірте файл юніта на очевидні помилки

cr0x@server:~$ systemd-analyze verify /etc/systemd/system/acme-api.service
/etc/systemd/system/acme-api.service:6: Unknown lvalue 'Wants' in section 'Unit'

Значення: У реальному житті трапляються опечатки. Тут воно виявило хибний ключ (приклад). Systemd може ігнорувати те, що ви вважали критичним.

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

Завдання 10: Перевірте, чи якась залежність не падає (монтування та сховище — часті винуватці)

cr0x@server:~$ systemctl --failed --no-pager
  UNIT                          LOAD   ACTIVE SUB    DESCRIPTION
● mnt-data.mount                loaded failed failed /mnt/data
● acme-api.service              loaded failed failed Acme API

LOAD   = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state.
SUB    = The low-level unit activation state.

Значення: Якщо сховище не змонтувалося, ваш додаток може бути колатеральною жертвою.

Рішення: Виправте монтування спочатку. Запуск додатку до появи шляху з даними — шлях до втрати даних, замаскованої під «доступність».

Завдання 11: Швидко огляньте неуспішний mount-юніт

cr0x@server:~$ systemctl status mnt-data.mount --no-pager -n 80
● mnt-data.mount - /mnt/data
     Loaded: loaded (/proc/self/mountinfo; generated)
     Active: failed (Result: exit-code) since Mon 2025-12-30 10:10:02 UTC; 4min ago
      Where: /mnt/data
       What: UUID=aa1b2c3d-4e5f-6789-a012-b345c678d901
    Process: 1222 ExecMount=/usr/bin/mount UUID=aa1b2c3d-4e5f-6789-a012-b345c678d901 /mnt/data (code=exited, status=32)
     Status: "Mounting failed."

Dec 30 10:10:02 server mount[1222]: mount: /mnt/data: wrong fs type, bad option, bad superblock on /dev/sdb1, missing codepage or helper program, or other error.
Dec 30 10:10:02 server systemd[1]: mnt-data.mount: Mount process exited, code=exited, status=32/n/a

Значення: класична помилка монтування. Може бути неправильний UUID, пошкоджена ФС, відсутній модуль ядра або диск змінився.

Рішення: Підтвердіть блоковий пристрій, виконайте blkid, перевірте dmesg, і лише потім використовуйте інструменти для ремонту файлової системи.

Завдання 12: Підтвердіть, чи AppArmor не заблокував сервіс (Ubuntu любить AppArmor)

cr0x@server:~$ journalctl -k -b --no-pager | grep -i apparmor | tail -n 5
Dec 30 10:11:19 server kernel: audit: type=1400 audit(1735553479.112:88): apparmor="DENIED" operation="open" class="file" profile="/usr/local/bin/acme-api" name="/etc/acme/secret.key" pid=1842 comm="acme-api" requested_mask="r" denied_mask="r" fsuid=1001 ouid=0

Значення: Ваш сервіс може працювати, а політика — ні.

Рішення: Оновіть профіль AppArmor (або припиніть обмежувати цей бінарник, якщо не можете його підтримувати). Не робіть chmod 777 секретам «щоб подивитись».

Завдання 13: Відтворіть виконання під обмеженнями, схожими на systemd (трансїєнтний юніт)

cr0x@server:~$ sudo systemd-run --unit=acme-api-debug --property=User=acme --property=Group=acme /usr/local/bin/acme-api --config /etc/acme/api.yaml
Running as unit: acme-api-debug.service
cr0x@server:~$ systemctl status acme-api-debug.service --no-pager -n 30
● acme-api-debug.service - /usr/local/bin/acme-api --config /etc/acme/api.yaml
     Loaded: loaded (/run/systemd/transient/acme-api-debug.service; transient)
     Active: failed (Result: exit-code) since Mon 2025-12-30 10:15:22 UTC; 2s ago
    Process: 2044 ExecStart=/usr/local/bin/acme-api --config /etc/acme/api.yaml (code=exited, status=1/FAILURE)

Значення: Це відрізняє «працює в моєму shell» від «працює під systemd в user/cgroup».

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

Завдання 14: Показати точну інтерпретацію коду виходу

cr0x@server:~$ systemctl show acme-api.service -p ExecMainStatus -p ExecMainCode -p Result
ExecMainStatus=1
ExecMainCode=exited
Result=exit-code

Значення: помилка з кодом виходу, не таймаут, не сигнал, не watchdog.

Рішення: Фокусуйтеся на stderr додатка й конфігурації. Не витрачайте час на залежності, якщо логи не вказують на них.

Завдання 15: Перевірте, чи сокети/порти були справжньою проблемою

cr0x@server:~$ ss -ltnp | grep -E ':8080\b'
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*    users:(("nginx",pid=912,fd=12))

Значення: Щось інше займає порт. Багато служб логують це, але іноді не встигають до виходу.

Рішення: Вирішіть конфлікт порту (змініть конфіг, зупиніть інший сервіс або використайте socket activation правильно).

Завдання 16: Коли залучено завантаження, перевірте попереднє завантаження теж

cr0x@server:~$ journalctl -u acme-api.service -b -1 --no-pager -n 80
Dec 30 09:03:12 server systemd[1]: Starting acme-api.service - Acme API...
Dec 30 09:03:13 server acme-api[701]: connecting to postgres at 10.20.0.15:5432
Dec 30 09:03:14 server acme-api[701]: ready
Dec 30 09:03:14 server systemd[1]: Started acme-api.service - Acme API.

Значення: Вчора все працювало. Це дар: щось змінилося (мережа, секрети, монти, політика, віддалена залежність).

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

Випадок №2: ланцюг залежностей + таймаут + оманливий «зелений» чек

Ось ситуація, яка зʼявляється у реальних кластерах: служба не стартує після планового перезавантаження, і помилка виглядає локальною.
Насправді — ні. Це проблема залежності/порядку в одежі таймауту.

Налаштування

Маєте API-сервіс, який читає конфіг і підключається до Postgres. Юніт встановлений як Type=notify.
Він налаштований з After=network.target і Wants=network.target.
На Ubuntu 24.04 мережу керує systemd-networkd або NetworkManager залежно від збірки.

Відмова починається так:

cr0x@server:~$ systemctl status acme-api.service --no-pager -n 30
● acme-api.service - Acme API
     Loaded: loaded (/etc/systemd/system/acme-api.service; enabled; preset: enabled)
     Active: failed (Result: timeout) since Mon 2025-12-30 10:12:48 UTC; 7min ago
Dec 30 10:12:48 server systemd[1]: acme-api.service: start operation timed out. Terminating.
Dec 30 10:12:48 server systemd[1]: Failed to start acme-api.service - Acme API.

Люди часто одразу пропонують «збільшити TimeoutStartSec». Іноді це вірно. Частіше — це лінь.
Перш ніж змінювати: чому зайняло так багато часу?

Крок 1: Сервіс не «впав», він «не готовий»

Журнал показує, що він чекає Postgres. Це перша підказка: процес стартував і робить роботу.
Друга підказка — Type=notify. Це означає, що systemd чекає повідомлення про готовність, а таймаут — це стіна за часом.

Найпростіша перевірка: чи доступний Postgres?

cr0x@server:~$ ping -c 2 10.20.0.15
PING 10.20.0.15 (10.20.0.15) 56(84) bytes of data.

--- 10.20.0.15 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1003ms

Значення: Цей хост не може досягти IP БД. Це не баг додатку.

Рішення: Діагностуйте маршрутизацію, VLAN, фаєрвол або стан мережевої служби. Поки не торкайтеся додатку.

Крок 2: Брехня «мережа піднята» (network.target)

network.target в основному означає «стек управління мережею запущено», а не «у вас є маршрути й підключення».
Якщо вам потрібна «налаштована IP», зазвичай мається на увазі network-online.target — але ставте його, як до гострого соусу:
трохи допоможе; надто багато зіпсує вечерю.

Перевірте, що забезпечує готовність «online»:

cr0x@server:~$ systemctl status systemd-networkd-wait-online.service --no-pager -n 40
● systemd-networkd-wait-online.service - Wait for Network to be Configured
     Loaded: loaded (/usr/lib/systemd/system/systemd-networkd-wait-online.service; enabled; preset: enabled)
     Active: failed (Result: timeout) since Mon 2025-12-30 10:10:55 UTC; 9min ago
    Process: 876 ExecStart=/usr/lib/systemd/systemd-networkd-wait-online (code=exited, status=1/FAILURE)
Dec 30 10:09:25 server systemd[1]: Starting systemd-networkd-wait-online.service - Wait for Network to be Configured...
Dec 30 10:10:55 server systemd[1]: systemd-networkd-wait-online.service: start operation timed out. Terminating.
Dec 30 10:10:55 server systemd[1]: systemd-networkd-wait-online.service: Failed with result 'timeout'.

Значення: мережа не стала «онлайн» в межах таймауту wait. Ваш API не мав шансів, якщо йому потрібна БД.

Рішення: Не «виправляйте» API. Виправте, чому хост так і не досяг network-online. Зазвичай: неправильний netplan, відсутній VLAN, лінк вниз, відмова DHCP або перейменований інтерфейс.

Крок 3: Знайдіть реальну мережеву відмову (netplan та стан інтерфейсу)

cr0x@server:~$ ip -br link
lo               UNKNOWN        00:00:00:00:00:00
enp0s31f6        DOWN           3c:52:82:aa:bb:cc

Значення: інтерфейс опущено. Це фізичний лінк, драйвер або проблема комутатора.

Рішення: Якщо це віртуальна машина, перевірте vNIC у гіпервізорі. Якщо фізичний — перевірте кабелі/порт комутатора та dmesg на предмет проблем з драйвером.

cr0x@server:~$ journalctl -u systemd-networkd -b --no-pager -n 120
Dec 30 10:09:03 server systemd-networkd[612]: enp0s31f6: Link DOWN
Dec 30 10:09:04 server systemd-networkd[612]: enp0s31f6: Lost carrier
Dec 30 10:09:05 server systemd-networkd[612]: enp0s31f6: DHCPv4 client: No carrier

Значення: немає carrier; DHCP не запустився. Повідомлення «Failed to start» тут невинне — ваша мережа ні.

Рішення: Відновіть лінк. Якщо лінк навмисно відсутній (ізольована мережа), тоді дизайн сервісу неправильний: він не повинен блокувати завантаження.

Крок 4: Оманливий «зелений» чек

У багатьох організаціях хтось проводить перевірку здоровʼя, яка показує «мережа ОК», бо резольвиться localhost або доступний локальний шлюз.
Це не означає підключення до вашої залежності. Це часткова істина.

Ось чек, що виглядає зелено, але не є таким:

cr0x@server:~$ getent hosts postgres.internal
10.20.0.15      postgres.internal

Значення: DNS (або /etc/hosts) працює. Це нічого не каже про маршрутизацію, фаєрвол чи лінк.

Рішення: Завжди поєднуйте перевірки резолюції з перевірками доступності: ip route get, ping, nc, ss.

cr0x@server:~$ ip route get 10.20.0.15
RTNETLINK answers: Network is unreachable

Значення: ядро не має маршруту. Це нижчий рівень ніж додаток і дає вищу впевненість, ніж «має бути добре».

Рішення: Виправте netplan/маршрути. Не чіпайте TimeoutStartSec, поки хост не зможе маршрутизувати до своїх залежностей.

Крок 5: Реальне виправлення (і не-виправлення)

Не-виправлення: підвищити TimeoutStartSec до пʼяти хвилин. Це лише дає вашому простою більше часу, щоб залишатися загадковим.

Виправлення — відновити мережу, а потім звузити поведінку сервісу:
якщо Postgres недоступний, чи повинен хост завантажуватися? Зазвичай — так. Чи повинен сервіс продовжувати повторні спроби? Зазвичай — так.
Чи повинен він блокуватися 90 секунд і потім вмерти? Це залежить від того, чи ви хочете мати юніт «active» без БД.

На практиці ви обираєте одне:

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

Якщо демон не підтримує sd_notify, перехід на Type=simple часто запобігає хибним таймаутам:

cr0x@server:~$ sudo systemctl edit acme-api.service
# (editor opens)
cr0x@server:~$ cat /etc/systemd/system/acme-api.service.d/override.conf
[Service]
Type=simple
TimeoutStartSec=30
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart acme-api.service

Значення: Ви узгоджуєте очікування готовності systemd з реальністю. Також ви скоротили вікно «загадкового зависання».

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

Жарт №2: Підвищення TimeoutStartSec — це як відсунути сигнал пожежної сигналізації подалі. Кухня все ще в вогні; ви просто гірше чуєте сигнал.

Три корпоративні міні-історії (і урок, який вам справді потрібен)

Міні-історія №1: Відмова через неправильне припущення

Середній SaaS мав бекграунд-воркер, який обробляв події білінгу. Він був «простий»:
читав з черги, писав у Postgres, емутував метрики. Працював місяцями без драм.

Під час спринту з жорсткішою безпекою хтось оновив файл юніта:
After=network.target і Wants=network.target, бо це виглядало «правильно» і вже було в інших юнітах.
Припущення було, що network.target означає «мережа піднята».

Через тиждень компанія мігрувала VLAN і додала довший чек DHCP на деяких вузлах.
Ці вузли завантажувалися, воркер стартував відразу, і він намагався підʼєднатися до маршруту до появи маршруту.
У воркера був таймаут старту 60 секунд і він виходив з кодом. systemd сильно перезапускав його. StartLimitHit спрацював.

Найнеприємніше: дашборди показували воркер «running» на більшості вузлів. На постраждалих вузлах він був «failed», але ніхто не мав алертів щодо стану юніта.
Проблема з білінгом помітила фінансова команда раніше за інженерів — особливий вид сорому.

Вони вирішили зробити підключення явним: або чекати конкретну залежність (доступність ендпоінта БД через скрипт),
або запускати воркер і дозволяти йому повторюватися без втрати готовності systemd. Також вони перестали використовувати network.target як заспокійливу ковдру.

Урок: Не кодуйте фольклор у файли юнітів. Якщо залежите від підключення — визначайте, що означає «готово», і вимірюйте це.

Міні-історія №2: Оптимізація, яка відгукнулася боком

Команда корпоративного ІТ хотіла пришвидшити завантаження флоту Ubuntu-серверів. Хтось помітив кілька секунд у «wait online»
і вирішив, що це непотрібно. Вони відключили systemd-networkd-wait-online.service, щоб зекономити час старту.

Це спрацювало — завантаження стало швидше. Потім сервіс, залежний від сховища, почав падати після перезавантажень. Не завжди. Але досить часто, щоб коштувати дорого.
Сервіс монтував iSCSI LUN, потім запускав базу даних зверху. З вимкненим wait-online, iSCSI логін змагався з конфігурацією мережі.
Іноді він мав час, іноді — ні.

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

Остаточне виправлення не було «включити wait-online глобально». Вони прописали порядок і вимоги точно:
юніт бази вимагав mount-юніт; mount-юніт вимагав мережевого маршруту; і вони використали перевірки залежностей на рівні юніта.
Час завантаження залишився швидким на машинах, яким мережа не потрібна. Машини, яким потрібна — стали правильними.

Урок: Глобальні оптимізації завантаження треба трактувати як зміну бібліотеки. Якщо ви не можете описати граф залежностей — не «прискорюйте».

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

Платформна команда керувала набором внутрішніх сервісів з вендорними файлами юнітів. Вони ніколи не редагували файли в /usr/lib/systemd/system.
Жодного разу. Натомість кожна зміна йшла у drop-in overrides в /etc/systemd/system/*.d/.

Це виглядало бюрократично для деяких інженерів — поки в пʼятницю ввечері не впав терміновий апдейт пакету.
Оновлення замінило вендорний юніт і змінило ExecStart прапорець. На хостах, де люди редагували вендорні файли напряму,
їхні зміни були безшумно перезаписані і сервіси впали.

На хостах, якими керували по нудному способу, drop-in все ще застосовувалися коректно. Після оновлення сервіси перезапустилися з новими вендорними дефолтами
плюс їхніми оверрайдами. Єдиною роботою було перевірити поведінку — не відновлювати збій конфігурації.

Постінцидентний огляд був тихим. Команда не отримала балів за героїзм. Вони уникли героїзму. Оце й є робота.

Урок: Нудні практики (drop-in, verify, reload, послідовні логи) — це ті, що переживають оновлення.

Типові помилки: симптом → корінна причина → виправлення

1) Симптом: «Failed with result ‘timeout’» під час старту

Корінна причина: невідповідність готовності (Type=notify без sd_notify), або сервіс блокується на залежності (БД, DNS, монтування), або TimeoutStartSec занадто короткий для реальної роботи.

Виправлення: Визначте, чи процес виконує корисну роботу. Якщо немає підтримки sd_notify — переключіть на Type=simple.
Якщо він чекає залежності — виправте залежність або змініть поведінку сервісу, щоб повторюватися після старту, а не блокувати готовність.

2) Симптом: «failed at step EXEC»

Корінна причина: шлях ExecStart неправильний, бінарник відсутній, неправильні дозволи, відсутній інтерпретатор (наприклад, скрипт з хибним shebang), або файлова система не змонтована.

Виправлення: Перевірте systemctl status і файл юніта; переконайтесь, що файл існує і є виконуваним; перевірте юніти монтування; перегляньте AppArmor-відмови.

3) Симптом: «start request repeated too quickly» / start-limit-hit

Корінна причина: цикл падіння або швидкі відмови, загострені Restart=always/on-failure. Лімітер systemd спрацьовує.

Виправлення: Читайте логи для першої відмови, а не останньої. Виправте підлягаючу конфігурацію/порт/секрети. Потім виконайте systemctl reset-failed.
За потреби налаштуйте StartLimit, але не використовуйте його, щоб приховати цикл.

4) Симптом: сервіс працює при запуску вручну, але падає під systemd

Корінна причина: інше середовище (PATH, робоча директорія, ulimits), інший користувач, опції sandboxing, відсутні дозволи.

Виправлення: Використайте systemd-run з User/Group, щоб відтворити; перевірте WorkingDirectory, EnvironmentFile і параметри жорсткого обмеження.

5) Симптом: сервіс падає лише після перезавантаження, не після ручного рестарту

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

Виправлення: Використайте systemd-analyze critical-chain і перевірте залежності. Замініть розмите After=network.target на правильний порядок і явні вимоги (mount-юніт, конкретний dependency-юніт або легкий pre-check).

6) Симптом: сервіс «стартує», але одразу виходить і systemd повідомляє успіх

Корінна причина: неправильний Type (наприклад, forking vs simple), або ExecStart запускає wrapper, який виходить, а демон не продовжує (або ніколи не продовжує).

Виправлення: Перевірте модель демона. Використовуйте Type=forking з PIDFile лише коли демон реально форкується. Віддавайте перевагу Type=simple або notify для сучасних демонів.

7) Симптом: усе «виглядає нормально», але сервіс не може отримати доступ до файлу або сокета

Корінна причина: відмова AppArmor, sandboxing systemd (ProtectSystem, ReadOnlyPaths, PrivateTmp), або невідповідність прав власності після деплою.

Виправлення: Шукайте відмови в журналi ядра; налаштуйте політику або sandbox; забезпечте правильні права власності й дозволи.

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

Чек-лист A: 90-секундний тріаж (один невдалий сервіс)

  1. Запустіть systemctl status UNIT --no-pager. Зафіксуйте Result (timeout/exit-code/start-limit-hit) і будь-який «крок» (EXEC).
  2. Запустіть journalctl -u UNIT -b -n 200 --no-pager. Знайдіть останній змістовний рядок логу додатка.
  3. Запустіть systemctl cat UNIT. Зверніть увагу на Type=, TimeoutStartSec=, User= і шлях ExecStart.
  4. Розподіліть за відрами: EXEC-проблема, exit-code, timeout/очікування, залежність, політика або обмеження частоти.
  5. Виберіть наступний інструмент на основі відра (не фантазуйте).

Чек-лист B: Робочий процес «завантаження застрягло»

  1. systemctl --failed --no-pager для переліку найочевидніших ранніх відмов (монти, мережа, резолюція імен).
  2. systemd-analyze critical-chain для визначення того, що затримало досягнення дефолтного таргету.
  3. Виправте найранішу зламану залежність спочатку (монтування/мережа) перед перезапуском нижчестоячих сервісів.
  4. Якщо потрібно підняти хост негайно: тимчасово замаскуйте некритичний невдалий юніт, завантажтесь у multi-user і скасуйте маску після виправлення.

Чек-лист C: Дисципліна ремонту безпечна для перезавантаження (уникнення самозавданих проблем)

  1. Ніколи не редагуйте вендорні файли юнітів у /usr/lib/systemd/system або /lib/systemd/system. Використовуйте drop-in через systemctl edit.
  2. Після змін у юніті: systemctl daemon-reload, потім перезапустіть юніт.
  3. Використовуйте systemd-analyze verify для юнітів, які ви змінювали.
  4. Записуйте, що й чому ви змінили. Майбутній ви — теж SRE, і йому буде важко без цього.

Чек-лист D: Запуски сервісів, що залежать від сховища (бо збої сховища маскують як відмови сервісів)

  1. Якщо сервіс використовує шлях на кшталт /mnt/data, забезпечте наявність mount-юніта і вимогу сервісу до нього.
  2. Перевіряйте невдалі монтування рано за допомогою systemctl --failed.
  3. Ніколи не дозволяйте базі даних ініціалізуватися на незмонтованому каталозі. Додайте явні перевірки або вимоги.
  4. Якщо монтування віддалене (NFS/iSCSI), розглядайте його як мережеву залежність і проектуйте на часткові відмови.

FAQ

1) Чому systemd каже «Failed to start», коли бінарний файл явно запустився?

Бо «start» включає готовність. Якщо використовується Type=notify, systemd очікує сигнал готовності.
Якщо додаток чекає залежності, systemd може таймаутнути і вбити його, хоч він «щось робив».

2) Чи завжди мені додавати After=network-online.target?

Ні. Багатьом сервісам не потрібна повна мережа під час старту, і чек online може уповільнити завантаження або ввести нові режими відмов.
Додавайте його лише коли сервіс справді потребує мережевого підключення для функціонування, і надавайте перевагу більш конкретним залежностям.

3) У чому різниця між After= і Requires=?

After= — тільки порядок: «запусти мене пізніше». Requires= — це вимога: «якщо те не вдасться — я впаду теж».
Плутання їх призводить або до дедлоків при завантаженні (надто багато вимог), або до умов гонки (порядок без гарантії наявності залежності).

4) Що зазвичай означає «failed at step EXEC»?

systemd не зміг виконати налаштовану команду. Поширені причини: неправильний шлях, відсутній бінарник, відсутній біт виконання, неправильні права користувача,
відсутній інтерпретатор для скриптів або невідповідний шлях у файловій системі.

5) Як зрозуміти, чи AppArmor блокує мій сервіс?

Шукайте відмови в журналі ядра: journalctl -k -b | grep -i apparmor.
Якщо бачите DENIED-повідомлення, що відповідають бінарнику вашого сервісу — це проблема політики, а не додатку.

6) Коли слід збільшувати TimeoutStartSec?

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

7) Чому цикл перезапусків сервісу переходить у start-limit-hit?

systemd обмежує перезапуски, щоб уникнути трешу. Якщо юніт падає багаторазово за короткий інтервал, спроби зупиняються.
Виправте підлягаючу проблему, потім очистіть лічильник за допомогою systemctl reset-failed.

8) Сервіс працює, коли я запускаю вручну. Чому він не запускається через systemd?

systemd запускає його з налаштованим користувачем, конкретним середовищем, робочою директорією, ulimits, обмеженнями cgroup і можливо sandboxing.
Відтворіть через systemd-run з тим самим User/Group, щоб звузити різницю.

9) Як зрозуміти, чи проблема від upstream (залежність), а не сервісу?

Якщо логи юніта показують «waiting for …» (БД, DNS, монтування), або якщо ви бачите невдалі mount/мережні юніти в systemctl --failed,
вважайте це upstream проблемою, поки не доведете протилежне. Виправляйте найранішу відмову в ланцюгу.

Наступні кроки, які можна зробити сьогодні

Якщо хочете, щоб інциденти «Failed to start» перестали їсти ваші післяобіди, зробіть ці кроки у порядку:

  1. Уніфікуйте команди тріажу у команді: systemctl status, journalctl -u, systemctl cat, systemd-analyze critical-chain, systemctl --failed.
  2. Зробіть готовність юнітів чесною: не використовуйте Type=notify, якщо демон дійсно не повідомляє; не маскуйте очікування залежностей довгими таймаутами.
  3. Пропишіть залежності явно: якщо потрібен mount — вимагайте mount. Якщо потрібен маршрут — доведіть його (або проектуйте повтори без блокування завантаження).
  4. Використовуйте drop-in для кожної операційної зміни. Зберігайте вендорні юніти чистими, щоб оновлення не дивували вас.
  5. Додайте алерти на падіння юнітів для критичних сервісів. Журнал чудовий, але він не запише вам sms.

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

← Попередня
Ubuntu 24.04 міст/VLAN для віртуалізації: виправте «VM немає інтернету» правильно
Наступна →
Mars Climate Orbiter: Розбіжність одиниць, яка призвела до втрати космічного апарата

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