MAATRIX / Блог / Как работает systemd при загрузке: дерево зависимостей за 8 секунд

Как работает systemd при загрузке: дерево зависимостей за 8 секунд

MAATRIX

Сервер перезагрузился — и сайт не поднялся. Заходите по SSH, запускаете systemctl start руками — всё работает. Значит, дело не в самом сервисе, а в том, когда и в каком порядке systemd его запускает при старте системы. Разберём, как устроен boot-процесс systemd на уровне targets и unit-файлов, и как найти, что именно тормозит или ломает загрузку — с конкретными командами.

От SysV init к systemd: что изменилось в загрузке

В классическом SysV init загрузка — строго последовательный список скриптов в /etc/rc.d/rcN.d/, пронумерованных вроде S20network, S55sshd. Init идёт по номерам по порядку, один скрипт ждёт завершения предыдущего. Даже если два сервиса друг от друга не зависят, они всё равно стартуют один за другим.

systemd (стандарт в Ubuntu, Debian, AlmaLinux и большинстве современных дистрибутивов) устроен иначе. Вместо линейного списка — граф зависимостей между unit-файлами, который планировщик разворачивает максимально параллельно: если сервис А не требует явно сервис Б, оба стартуют одновременно.

Побочный эффект: раз порядок больше не задаётся номером файла, а выводится из графа зависимостей — вся логика «что после чего» переносится в конфигурацию каждого unit-файла. Отсюда и большинство проблем «не стартует сам после перезагрузки»: если зависимость не описана явно, systemd не обязан её соблюдать, даже если раньше всё как-то работало «по случайному порядку».

Targets: аналог runlevel, но не совсем

В SysV init загрузка шла по runlevel'ам. В systemd им соответствуют targets — но target не «уровень», а группа юнитов, объединённых по смыслу.

TargetАналог runlevelЧто означает
poweroff.target0Выключение системы
rescue.target1Однопользовательский режим восстановления
multi-user.target3Многопользовательский режим без GUI — то, что нужно на VPS
graphical.target5Многопользовательский режим с графикой
reboot.target6Перезагрузка

Какой target загружается по умолчанию:

systemctl get-default

На сервере без графики почти всегда multi-user.target. Если по ошибке стоит graphical.target на голом сервере — загрузка виснет в ожидании графических юнитов, которых нет:

systemctl set-default multi-user.target

Targets связаны цепочкой: graphical.target требует multi-user.target, тот — basic.target, а он — sysinit.target. Ваши сервисы (nginx, postgresql, docker) обычно цепляются на multi-user.target через WantedBy=multi-user.target в своём unit-файле.

Что входит в target и в каком состоянии:

systemctl list-dependencies multi-user.target

Покажет дерево зависимостей — удобно понять, через какую цепочку Wants=/Requires= конкретный сервис попал в загрузку.

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

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

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

Unit-файлы: из чего состоит система

Everything is a unit. Основные типы на сервере:

  • .service — демон: nginx, postgresql, docker, ваше приложение.
  • .socket — сокет, который может поднимать сервис по требованию (socket activation).
  • .mount / .automount — точки монтирования, в том числе автоматически сгенерированные из /etc/fstab.
  • .target — группа юнитов без своей логики запуска, точка синхронизации.
  • .timer — аналог cron, встроенный в systemd.

Unit-файлы лежат в нескольких местах:

  • /usr/lib/systemd/system/ (или /lib/systemd/system/) — файлы из пакетов. Их не редактируют напрямую — обновление пакета сотрёт изменения.
  • /etc/systemd/system/ — переопределения пакетных юнитов и ваши собственные.
  • /run/systemd/system/ — временные, генерируются при загрузке, не переживают перезагрузку.

Правильный способ поправить сервис из пакета, не трогая оригинал:

systemctl edit имя-сервиса.service

Команда открывает /etc/systemd/system/имя-сервиса.service.d/override.conf — туда пишутся только переопределяемые директивы. После правки обязательно:

systemctl daemon-reload
systemctl restart имя-сервиса.service

daemon-reload перечитывает unit-файлы с диска — без него systemd продолжит работать со старой версией конфигурации в памяти.

Граф зависимостей: Wants, Requires, After, Before

Здесь чаще всего прячется причина «сервис не стартует сам». В секции [Unit] четыре директивы решают две разные задачи:

Порядок (когда, но не «нужен ли»):

  • After= — стартовать после указанного unit'а (не гарантирует, что тот вообще запустится).
  • Before= — стартовать раньше указанного.

Зависимость (нужен ли вообще, но не «когда»):

  • Wants= — «неформальная» зависимость: если указанный unit не запустился, наш всё равно попробует стартовать.
  • Requires= — «жёсткая» зависимость: если указанный unit падает, наш тоже останавливается.

Ключевая грабля: After= без Wants=/Requires= задаёт только порядок относительно другого юнита, но не гарантирует, что тот вообще был запущен к этому моменту. Поэтому в рабочих конфигах обычно видите пару вместе:

[Unit]
After=network-online.target
Wants=network-online.target

Типичный паттерн для сервисов, которым при старте нужна сеть. Именно здесь чаще всего живёт классическая проблема «запускается вручную, но не поднимается при загрузке»: сервис указывает After=network.target (юнит означает лишь «сетевой стек ядра инициализирован»), а не After=network-online.target (ждёт, пока интерфейс реально получит адрес через NetworkManager или systemd-networkd). network.target может быть «достигнут» ещё до того, как DHCP отработал. Сервис стартует, пытается резолвить DNS или подключиться к БД — и падает, потому что сети физически ещё нет. При ручном запуске минутой позже сеть уже есть — и всё работает.

Проверить зависимости юнита:

systemctl show имя-сервиса.service -p After -p Wants -p Requires

Если видите network.target вместо network-online.target — вот кандидат на правку через systemctl edit:

[Unit]
After=network-online.target
Wants=network-online.target

Для этого сама network-online.target должна быть активирована (в Ubuntu/Debian — systemd-networkd-wait-online.service или NetworkManager-wait-online.service, в зависимости от того, что настраивает сеть).

Похожая логика — для сервисов, зависящих от базы на этой же машине: если приложение стартует раньше, чем БД успела поднять сокет, первое подключение падает. Это же справедливо и когда после ребута не поднимается MySQL/MariaDB — сама база может не успеть закончить recovery до того, как systemd посчитает её target «достигнутым» (подробнее — в статье про то, почему MySQL не запускается после перезагрузки).

systemd-analyze: где теряется время при загрузке

Общее время загрузки:

systemd-analyze
Startup finished in 2.1s (kernel) + 1.4s (userspace) = 3.5s

Показывает инициализацию ядра отдельно от времени на запуск всех systemd-юнитов. Дальше — два инструмента для разбора.

systemd-analyze blame — плоский список юнитов по времени, которое каждый потратил на собственный запуск:

systemd-analyze blame
8.021s cloud-init.service
2.114s docker.service
1.897s snapd.seeded.service

Важно: это время самого юнита, а не то, сколько он держал всю загрузку. Если два тяжёлых юнита стартовали параллельно, оба попадут в верх списка, но реального замедления цепочки не давали.

Для этого нужна вторая команда — systemd-analyze critical-chain, которая строит цепочку, реально определившую итоговое время:

systemd-analyze critical-chain
graphical.target @3.412s
└─multi-user.target @3.410s
  └─docker.service @1.298s +2.112s
    └─network-online.target @1.296s

Читается снизу вверх. Число со знаком + — вклад именно этого юнита в общее время. Здесь docker.service держал загрузку +2.112s — он и тормозит цепочку. На вашем сервере в critical-chain окажется другой юнит с другим временем — ориентируйтесь на свои цифры, универсальных «нормальных» секунд не существует, конфигурация и железо у всех разные.

Для конкретного юнита можно построить цепочку целиком до него:

systemd-analyze critical-chain docker.service

Диагностика: сервис стартует руками, но не сам

Когда проблемный юнит найден, чек-лист простой:

  1. Статус после последней попытки автозапуска:
   systemctl status имя-сервиса.service

Смотрите не только на active/failed, но и на Loaded: — бывает, что сервис disabled (systemctl is-enabled имя-сервиса.service), и тогда он и не должен был стартовать сам — решает systemctl enable.

  1. Логи юнита за время загрузки:
   journalctl -u имя-сервиса.service -b

Флаг -b — только текущая загрузка. Для предыдущей — journalctl -u имя-сервиса.service -b -1.

  1. Что реально указано в зависимостях:
   systemctl show имя-сервиса.service -p After -p Wants -p Requires -p Requisite
  1. Порядок событий по всей загрузке, если непонятно состояние системы в нужный момент:
   journalctl -b --no-pager | grep -i "имя-сервиса\|network\|Reached target"

Строки Reached target ... — маркеры, когда каждый target был фактически достигнут; сопоставляя их по времени со стартом проблемного сервиса, обычно сразу видно, чего он не дождался.

Частая находка: сервис зависит не от сети, а от точки монтирования — данные лежат на отдельном диске или сетевой шаре, которая монтируется позже. Тогда нужный юнит для After=/Requires= — не network-online.target, а сгенерированный systemd .mount-юнит (посмотреть — systemctl list-units -t mount).

Socket activation и параллельность без явных зависимостей

Отдельный механизм — socket activation. Вместо явной зависимости «сервис Б стартует после сервиса А, потому что А слушает порт» systemd сам создаёт сокет заранее и держит его открытым — подключаться к нему уже можно, а реальный демон поднимется чуть позже, когда придёт первое соединение или дойдёт очередь по графу.

Пример — Docker слушает через docker.socket, в некоторых дистрибутивах так же настроен SSH (ssh.socket + ssh@.service). Это позволяет не выстраивать строгую цепочку: сокет создаётся почти мгновенно на этапе sysinit.target, а тяжёлая инициализация демона идёт параллельно с остальной загрузкой.

Какие сокеты активны:

systemctl list-sockets

Если сервис вроде бы должен стартовать, но в list-dependencies явной стрелки нет — вероятно, он поднимается через socket activation.

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

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

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

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

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

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

Чем network.target отличается от network-online.target?

network.target достигается, когда сетевой стек ядра просто инициализирован, без гарантии, что интерфейс получил адрес и линк поднят. network-online.target ждёт подтверждения от конкретной службы (systemd-networkd, NetworkManager, dhcpcd), что интерфейс готов к работе. Для сервисов, которым при старте нужен реальный доступ в сеть, нужен именно второй.

Обязательно ли использовать Requires= вместе с After=?

Если без зависимого сервиса ваш всё равно не имеет смысла работать (без БД приложение упадёт на первом запросе) — Requires= логичнее. Но на практике Wants= часто безопаснее: Requires= не даст стартовать вообще ничего, если зависимость временно недоступна, а Wants= хотя бы попробует.

Почему blame и critical-chain показывают разные "тяжёлые" юниты?

blame — отсортированный список времени каждого юнита самого по себе, без учёта критического пути. critical-chain — именно цепочка событий, определившая итоговое время до финального target. Юнит может быть долгим по blame, но не входить в critical-chain, если запускался параллельно с чем-то ещё более долгим.

Как отменить правки, сделанные через systemctl edit?

Удалите /etc/systemd/system/имя-сервиса.service.d/override.conf (и пустой каталог .d, если остался), затем systemctl daemon-reload. Оригинальный unit-файл пакета не менялся и останется рабочим состоянием по умолчанию.

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

systemd-analyze без флагов показывает только последнюю загрузку. История доступна через journalctl --list-boots (список сохранённых загрузок с номерами) и дальше journalctl -b -N для конкретной — но численно сравнить время между перезагрузками эта команда не даёт, для этого нужно смотреть таймстампы Reached target руками.

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

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

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