systemd вглубь: units, targets, зависимости

Разбираем systemd не как «команду systemctl», а как менеджер зависимостей: из чего состоят юниты, как они связаны и почему таймеры вытесняют cron.

Unit — текстовый файл-описание ресурса (службы, сокета, точки монтирования, таймера), которым управляет systemd как PID 1; зависимости между юнитами образуют граф загрузки.

Зачем это на практике

Базовый systemctl start nginx вы уже умеете. Но в реальной работе нужно написать свою службу для приложения, заставить её стартовать после базы данных, перезапускаться при падении и подниматься при загрузке. Без понимания типов юнитов и директив Wants/Requires/After вы будете копировать чужие .service-файлы наугад и удивляться, почему служба «не видит» сеть или стартует слишком рано.

Типы юнитов

systemd управляет не только службами. Расширение файла задаёт тип:

ТипЧто описывает
.serviceпроцесс/демон (запуск, перезапуск, окружение)
.socketсетевой или Unix-сокет; служба поднимается по первому соединению
.timerрасписание запуска другого юнита (замена cron)
.targetгруппа юнитов = точка синхронизации (аналог runlevel)
.mount / .automountмонтирование ФС
.pathреакция на появление/изменение файла

Посмотреть всё загруженное и его тип:

systemctl list-units --type=service --state=running
systemctl list-unit-files --type=timer

Targets вместо runlevel

target — это просто набор зависимостей, удобная «остановка» в графе загрузки. Исторические runlevel отображаются на цели:

ЦельСмысл
multi-user.targetсервер без графики (бывший runlevel 3)
graphical.targetс графической оболочкой (runlevel 5)
rescue.targetоднопользовательский режим восстановления
systemctl get-default
systemctl set-default multi-user.target   # не грузить графику
systemctl isolate rescue.target            # перейти в режим спасения прямо сейчас

Пишем свой .service

Системные юниты лежат в /usr/lib/systemd/system/ (от пакетов), а ваши — в /etc/systemd/system/ (приоритетнее). Создадим службу для веб-приложения на Python:

sudo tee /etc/systemd/system/myapp.service <<EOF
[Unit]
Description=My Python App
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/python app.py
Restart=on-failure
RestartSec=5
Environment=PORT=8080

[Install]
WantedBy=multi-user.target
EOF

Разбор секций. [Unit] описывает порядок и зависимости. [Service] — как запускать процесс. [Install] — куда «прицепить» юнит при включении (enable).

Порядок и зависимости: After, Wants, Requires

Это место, где ошибаются чаще всего. Важно понимать: порядок и зависимость — разные оси.

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

Почти всегда нужна пара Wants= + After=: «желательно подними и сделай это до меня». Одно Requires= без After= — частый баг: зависимость поднимется, но параллельно с вами.

enable vs start

Их путают постоянно. Это два независимых действия:

systemctl start myappзапустить сейчас; после перезагрузки — нет
systemctl enable myappвключить автозапуск при загрузке; сейчас не трогает
systemctl enable --now myappи включить, и запустить сразу
sudo systemctl daemon-reload        # перечитать изменённые юниты
sudo systemctl enable --now myapp
systemctl status myapp
sudo systemctl restart myapp

После любой правки файла юнита обязателен daemon-reload — иначе systemd работает со старой версией в памяти.

Таймеры как замена cron

systemd timer — современная альтернатива cron. Преимущества: запуск пишется в общий журнал (journalctl), пропущенные из-за выключенной машины задания можно «догнать» (Persistent=true), а ресурсы задачи ограничиваются как у обычной службы. Таймер всегда работает в паре: foo.timer запускает одноимённый foo.service.

# 1) что запускать — backup.service (Type=oneshot)
sudo tee /etc/systemd/system/backup.service <<EOF
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
EOF

# 2) когда — backup.timer
sudo tee /etc/systemd/system/backup.timer <<EOF
[Unit]
Description=Run backup nightly

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

sudo systemctl enable --now backup.timer
systemctl list-timers --all

Синтаксис OnCalendar читается человеком: daily, Mon *-*-* 09:00:00, *:0/15 (каждые 15 минут). Проверить, как systemd понял выражение: systemd-analyze calendar "Mon *-*-* 09:00:00".

Как это работает под капотом

systemd запускается ядром как PID 1 и становится прародителем всех процессов. Он читает юниты, строит ориентированный граф зависимостей и поднимает то, что не зависит друг от друга, параллельно — отсюда быстрая загрузка. Каждая служба запускается в собственной cgroup: все её дочерние процессы остаются «внутри», поэтому systemctl stop надёжно гасит даже форкнутые процессы, а systemctl status точно показывает дерево. Сокет-активация работает так: systemd сам открывает порт (через .socket), и при первом подключении лениво стартует службу, передавая ей уже готовый файловый дескриптор — это и ускоряет загрузку, и позволяет перезапускать службу без потери соединений.

Частые ошибки

  • Забыли daemon-reload после правки — изменения не применились.
  • Перепутали start и enable: служба «пропадает» после перезагрузки, потому что её только запустили, но не включили.
  • Requires= без After=: зависимость есть, но порядка нет — служба стартует одновременно с базой и падает.
  • Указали network.target вместо network-online.target и удивляетесь, что при старте ещё нет IP (первая цель означает «сеть настраивается», вторая — «адрес получен»).
  • Type=simple для демона, который форкается в фон: systemd считает его упавшим. Для таких нужен Type=forking.

Итоги

  • Юнит — это описание ресурса; тип задаётся расширением (.service, .socket, .timer, .target).
  • target — точка синхронизации графа загрузки, замена runlevel.
  • Wants/Requires = зависимость, After/Before = порядок; обычно нужна пара Wants=+After=.
  • start = сейчас, enable = при загрузке; объединяет enable --now.
  • Таймеры заменяют cron: журналируются, умеют догонять пропуски и ограничивать ресурсы.
Проверьте себя
1. В чём разница между systemctl enable myapp и systemctl start myapp?
Aenable запускает службу сейчас, start включает автозапуск при загрузке
Benable включает автозапуск при загрузке (сейчас не запускает), start запускает сейчас (но не при загрузке)
CЭто синонимы, разницы нет
Dstart работает только для таймеров, enable — для служб
2. Служба должна стартовать только после postgresql, и без неё работать не имеет смысла. Какая пара директив корректна?
AТолько After=postgresql.service
BТолько Requires=postgresql.service
CRequires=postgresql.service и After=postgresql.service
DWants=postgresql.service без After=