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=timerTargets вместо 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: журналируются, умеют догонять пропуски и ограничивать ресурсы.