Администраторы серверов часто сталкиваются с ситуацией, когда система загружается медленнее, чем хотелось бы. Десятки сервисов запускаются в определенном порядке, некоторые ждут других, создавая узкие места. Традиционные init-системы предлагали последовательный запуск, где каждый сервис ждал завершения предыдущего. Systemd изменяет правила игры, предоставляя параллельный запуск с умным управлением зависимостями. Способность создавать сложные цепочки сервисов, где каждое звено знает свое место, превращает хаотичный процесс загрузки в симфонию точно выверенных действий.

Анатомия зависимостей и порядок запуска

Systemd разделяет два фундаментальных понятия, которые часто путают между собой. Зависимости определяют, какие юниты должны быть запущены вместе, а директивы порядка указывают последовательность их активации. Эти механизмы работают независимо и ортогонально друг другу. Зависимость Wants указывает, что при запуске первого юнита второй тоже будет запущен, но его провал не остановит основной сервис. Requires действует жестче, при провале зависимости основной юнит также терпит неудачу:

# /etc/systemd/system/webapp.service
[Unit]
Description=Web Application Service
Wants=database.service
After=database.service network.target
Requires=storage.mount

[Service]
Type=forking
ExecStart=/usr/local/bin/webapp-daemon
Restart=on-failure

[Install]
WantedBy=multi-user.target

В этом примере webapp.service хочет database.service, но не требует её обязательного успеха. Директива After гарантирует, что database.service и network.target полностью активируются перед запуском приложения. Однако After не создает зависимость, она только управляет порядком. Если вы хотите, чтобы юнит запустился до другого, используйте Before. Эти директивы работают как зеркальное отражение друг друга.

Создание правильных цепочек зависимостей требует понимания состояний юнитов. Каждый юнит проходит через состояния: inactive, activating, active, deactivating, failed. Systemd считает юнит активированным, когда все его ExecStart команды завершены. Для сервисов типа oneshot с RemainAfterExit=yes юнит остается active даже после завершения процесса:

# /etc/systemd/system/prepare-data.service
[Unit]
Description=Prepare application data
Before=webapp.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/prepare-data.sh

Директива PartOf создает одностороннюю зависимость для остановки и перезапуска. Когда юнит, указанный в PartOf, останавливается или перезапускается, действие распространяется на зависимый юнит. Это полезно для создания групп сервисов, управляемых через target:

# /etc/systemd/system/worker1.service
[Unit]
Description=Worker Process 1
PartOf=workers.target
After=workers.target

[Service]
ExecStart=/usr/local/bin/worker --id=1
Restart=always

Теперь остановка или перезапуск workers.target автоматически остановит все сервисы с PartOf=workers.target.

Targets как группировка и синхронизация точек

Targets в systemd представляют состояния системы или точки синхронизации, где группы юнитов должны достичь определенного состояния. В отличие от старых runlevels, targets более гибкие и могут быть активны одновременно. Multi-user.target представляет полнофункциональную систему без графического интерфейса, эквивалент старого runlevel 3. Graphical.target добавляет графическую среду, требуя multi-user.target как зависимость:

systemctl get-default
systemctl set-default multi-user.target
systemctl isolate graphical.target

Команда isolate переключает систему в указанный target немедленно, останавливая все юниты, не требующиеся этим target. Это работает только с targets, имеющими AllowIsolate=yes в конфигурации. Создание собственных targets позволяет группировать связанные сервисы:

# /etc/systemd/system/application-stack.target
[Unit]
Description=Complete Application Stack
Requires=database.service cache.service
Wants=monitoring.service
After=database.service cache.service

[Install]
WantedBy=multi-user.target

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

systemctl list-dependencies application-stack.target
systemctl list-dependencies --reverse multi-user.target

Флаг --reverse показывает, какие юниты зависят от указанного, что полезно для понимания влияния изменений. Targets также служат точками синхронизации в процессе загрузки. Network.target достигается, когда сетевые интерфейсы активированы, хотя это не гарантирует полную сетевую связность. Для ожидания полной готовности сети используется network-online.target:

[Unit]
Description=Service requiring network
After=network-online.target
Wants=network-online.target

Важно понимать, что targets не обязательно означают завершение всех зависимых сервисов. Они означают, что системa достигла определенной точки в последовательности загрузки.

Socket activation для ленивой загрузки сервисов

Socket activation представляет элегантный механизм запуска сервисов по требованию. Systemd создает и слушает сокет от имени сервиса. Когда приходит первое подключение, systemd запускает связанный сервис и передает ему готовый сокет. Это драматически ускоряет загрузку, откладывая старт редко используемых сервисов до момента реального обращения к ним:

# /etc/systemd/system/myapp.socket
[Socket]
ListenStream=127.0.0.1:8080
Accept=no
NoDelay=true

[Install]
WantedBy=sockets.target
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application Service
Requires=myapp.socket

[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
NonBlocking=true
StandardInput=socket

Параметр Accept=no указывает, что systemd передаст слушающий сокет сервису, который сам будет вызывать accept() для каждого подключения. Accept=yes создает новый экземпляр сервиса для каждого входящего соединения, подобно inetd. NonBlocking=true гарантирует, что файловые дескрипторы будут в неблокирующем режиме, что критично для многих асинхронных приложений.

Сервисы, поддерживающие socket activation нативно, могут получать сокеты через переменные окружения LISTEN_FDS и LISTEN_FDNAMES. Первая содержит количество переданных дескрипторов, вторая их имена:

# /etc/systemd/system/webserver.socket
[Socket]
ListenStream=80
ListenStream=443
FileDescriptorName=http:https

[Install]
WantedBy=sockets.target

Для сервисов, не поддерживающих socket activation изначально, можно использовать systemd-socket-proxyd. Этот инструмент принимает подключения на сокете и проксирует их к реальному сервису:

# /etc/systemd/system/legacy-proxy.service
[Unit]
Description=Socket proxy for legacy application
Requires=legacy-proxy.socket
After=legacy-proxy.socket

[Service]
ExecStart=/usr/lib/systemd/systemd-socket-proxyd 127.0.0.1:9000

Преимущества socket activation выходят за рамки ускорения загрузки. Если сервис падает, его сокет остается активным, буферизуя входящие подключения. После перезапуска сервис продолжает с того места, где остановился, не теряя ни одного сообщения. При обновлении сервиса можно перезапустить его, сохраняя сокеты, обеспечивая непрерывную доступность.

Анализ и оптимизация критической цепочки загрузки

Понимание того, какие юниты замедляют загрузку, начинается с systemd-analyze. Эта утилита предоставляет детальную информацию о времени, затраченном на каждом этапе запуска:

systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain
systemd-analyze plot > bootup.svg

Первая команда показывает общее время, разделенное на firmware, loader, kernel, initrd и userspace. Blame выводит список всех юнитов, отсортированных по времени инициализации. Однако эти цифры могут вводить в заблуждение, так как параллельный запуск означает, что сумма времен всех юнитов значительно превышает реальное время загрузки:

$ systemd-analyze blame
         12.5s postgresql.service
          8.2s NetworkManager-wait-online.service
          3.1s docker.service
          2.8s systemd-networkd.service

Critical-chain показывает критический путь загрузки, цепочку юнитов, которые реально задерживают достижение target. Время после символа @ указывает, когда юнит стал активным, время после + показывает, сколько заняла его активация:

$ systemd-analyze critical-chain
graphical.target @15.3s
└─multi-user.target @15.2s
  └─postgresql.service @2.7s +12.5s
    └─network.target @2.6s
      └─NetworkManager.service @1.2s +1.4s

В этом примере postgresql.service находится на критическом пути и добавляет 12.5 секунд к времени загрузки. Оптимизация заключается в том, чтобы либо ускорить этот сервис, либо сделать его запуск асинхронным через socket activation. Визуализация через plot создает SVG диаграмму, показывающую временную шкалу всех юнитов. Красный цвет выделяет период активации, что помогает визуально идентифицировать узкие места:

systemd-analyze plot > /tmp/boot.svg
firefox /tmp/boot.svg

Для углубленного анализа зависимостей используется команда dot, генерирующая граф в формате GraphViz:

systemd-analyze dot | dot -Tsvg > dependencies.svg

Цвета на графе имеют значение: черный означает Requires, темно-синий Requisite, темно-серый Wants, красный Conflicts, зеленый After. Это позволяет визуально проследить сложные цепочки зависимостей и найти проблемные связи.

Практическая оптимизация начинается с идентификации ненужных сервисов. Многие дистрибутивы включают сервисы, которые не требуются в конкретной конфигурации:

systemctl disable NetworkManager-wait-online.service
systemctl mask plymouth-start.service

Команда disable удаляет симлинки из директорий .wants, предотвращая автоматический запуск. Mask делает юнит полностью недоступным для запуска, даже вручную. Для сервисов, которые требуются, но не критичны для загрузки, можно изменить их тип на Type=idle. Такие сервисы запускаются только после того, как все активные задачи завершены, не блокируя критический путь.

Продвинутые паттерны управления зависимостями

Создание сложных сценариев требует комбинирования различных типов зависимостей. Для приложений, требующих многоступенчатую инициализацию, можно создать цепочку сервисов типа oneshot:

# /etc/systemd/system/app-init-db.service
[Unit]
Description=Initialize application database
Before=app-migrate.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/init-db.sh

# /etc/systemd/system/app-migrate.service
[Unit]
Description=Run database migrations
After=app-init-db.service
Before=app-server.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/migrate-db.sh

# /etc/systemd/system/app-server.service
[Unit]
Description=Application server
Requires=app-migrate.service
After=app-migrate.service

[Service]
Type=notify
ExecStart=/usr/local/bin/app-server
Restart=always

Тип notify указывает, что сервис отправит уведомление через sd_notify(), когда полностью готов к работе. Systemd будет ждать этого сигнала перед активацией зависимых юнитов. Для обработки сбоев используются директивы OnFailure и OnSuccess:

[Unit]
Description=Critical service with failure handling
OnFailure=notify-admin.service recovery.service

[Service]
Type=simple
ExecStart=/usr/local/bin/critical-app
Restart=on-failure
RestartSec=10
StartLimitBurst=5
StartLimitIntervalSec=60

Если сервис падает, systemd автоматически запустит юниты, указанные в OnFailure. StartLimitBurst и StartLimitIntervalSec предотвращают бесконечные циклы перезапуска, ограничивая количество попыток в заданный промежуток времени. Для создания взаимозависимых сервисов, где один не может работать без другого, используется BindsTo:

[Unit]
Description=Worker process
BindsTo=master.service
After=master.service

[Service]
ExecStart=/usr/local/bin/worker

BindsTo сильнее, чем Requires, он останавливает зависимый юнит, если основной останавливается, даже если это не связано со сбоем. Эффективное применение systemd требует глубокого понимания взаимодействия между зависимостями, порядком запуска и состояниями юнитов. Правильно спроектированная система запускается параллельно там, где возможно, соблюдает строгий порядок там, где необходимо, и изящно обрабатывает сбои, не распространяя их на здоровые компоненты. Комбинация socket activation, продуманных targets и точного анализа критического пути превращает медленную загрузку в быстрый и надежный процесс.