Как работает systemd: структура, юниты и как писать собственные сервисы

9 минут чтения
Средний рейтинг статьи — 4.8

systemd — это система инициализации и менеджер служб, с которой начинается загрузка почти любого современного Linux. Он запускает процессы, следит за их состоянием, перезапускает упавшие, собирает логи и разруливает зависимости между сервисами.

Разберём, как он устроен, какие бывают юниты, как написать собственную службу и что настроить, чтобы она вела себя предсказуемо в продакшене.

Что такое systemd и чем он заменил init

Раньше службами управлял SysVinit: набор shell-скриптов в /etc/init.d/, которые выполнялись строго по очереди. Скрипт сам отвечал за запуск демона, запись pid-файла и проверку, жив ли процесс, — и делал это в каждом пакете по-своему.

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

  • Параллельный запуск служб вместо последовательного — загрузка стала быстрее.
  • Настоящий контроль процессов через cgroups: systemd знает обо всех потомках службы и корректно завершает их, а не теряет «осиротевшие» процессы.
  • Единое логирование через journald, без разнобоя лог-файлов.
  • Автоперезапуск упавших служб без сторонних supervisord и monit.
  • Активация по требованию — через сокеты, таймеры или появление файла.

Юниты и их типы

Всё, чем управляет systemd, описывается юнитами — конфигурационными файлами, тип которых определяется расширением.

ТипРасширениеНазначение
Service.serviceСлужбы и демоны
Socket.socketАктивация по обращению к сокету
Timer.timerЗапуск по расписанию, замена cron
Mount.mountТочки монтирования
Path.pathРеакция на появление или изменение файла
Target.targetГруппа юнитов, аналог уровней загрузки

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

Структура .service файла

Минимальный юнит для приложения на Node.js:

[Unit]
Description=My Node.js App
After=network-online.target postgresql.service
Wants=network-online.target
 
[Service]
Type=simple
ExecStart=/usr/bin/node /var/www/myapp/server.js
WorkingDirectory=/var/www/myapp
User=www-data
Environment=NODE_ENV=production
EnvironmentFile=-/etc/myapp/env
Restart=always
RestartSec=5
 
[Install]
WantedBy=multi-user.target

Секции делят описание по смыслу: [Unit] — метаданные и зависимости, [Service] — как запускать процесс, [Install] — что делать при systemctl enable.

Дефис в EnvironmentFile=-/etc/myapp/env означает «файла может не быть, это не ошибка». Без него служба не стартует, если файл отсутствует.

Type: чем отличаются simple, exec, forking и другие

Параметр Type= объясняет systemd, как понять, что служба запустилась. Ошибка здесь — самая частая причина странного поведения юнита.

  • simple (по умолчанию) — процесс, указанный в ExecStart, и есть служба. systemd считает её запущенной сразу после fork(), не дожидаясь готовности.
  • exec — как simple, но статус «запущено» выставляется после успешного execve(). Ошибку вроде «нет такого файла» вы увидите сразу при systemctl start, а не в логах.
  • forking — для классических демонов, которые уходят в фон сами. Требует PIDFile=, иначе systemd потеряет процесс.
  • oneshot — процесс отрабатывает и завершается; полезно для миграций и разовых задач, обычно вместе с RemainAfterExit=yes.
  • notify — служба сама сообщает о готовности через sd_notify(). Самый честный вариант: зависимые сервисы стартуют, только когда приложение действительно готово принимать запросы.

Для веб-приложения на simple есть подвох: зависимые юниты стартуют, когда процесс ещё инициализируется. Если это важно, используйте notify или проверку готовности на уровне приложения — как это устроено, разбирали в статье про health check сервиса.

Зависимости: After, Requires, Wants

Эти директивы часто путают, а отвечают они за разное.

  • After= — только порядок: наш юнит запустится после указанного. Не требует, чтобы тот вообще существовал.
  • Requires= — жёсткая зависимость: если зависимость не поднялась, наш юнит не стартует. Порядок при этом не гарантируется, поэтому Requires= почти всегда пишут вместе с After=.
  • Wants= — мягкая: systemd попробует запустить зависимость, но её падение нашей службе не помешает. Разумный выбор по умолчанию.
  • BindsTo= — жёстче Requires=: если зависимость остановилась уже после запуска, наш юнит тоже будет остановлен.

Отдельная тонкость — network.target против network-online.target. Первый означает лишь, что сетевая подсистема настроена, а не что адрес получен. Приложениям, которым нужно сразу открывать соединения, требуется второй, причём вместе с Wants=network-online.target.

Перезапуск и устойчивость

Restart=always без ограничений — путь к бесконечному циклу перезапусков падающего приложения. Полезный набор параметров:

Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
TimeoutStopSec=30

Здесь служба перезапускается только при ненулевом коде выхода, с паузой в 5 секунд, но если за 5 минут случилось больше 5 попыток — systemd прекращает попытки и оставляет юнит в состоянии failed. Это лучше бесконечного цикла: сервис явно виден как сломанный, а не создаёт видимость работы.

TimeoutStopSec задаёт, сколько ждать корректного завершения перед SIGKILL. Если приложение умеет graceful shutdown, дайте ему на это время — иначе запросы будут обрываться при каждом деплое.

Изоляция и лимиты

systemd умеет ограничивать службу без контейнеров — несколько строк дают заметный выигрыш в безопасности:

NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myapp

ProtectSystem=strict монтирует всю файловую систему только на чтение, кроме явно разрешённых путей; PrivateTmp даёт службе собственный /tmp; NoNewPrivileges запрещает повышение привилегий. Проверить, насколько юнит защищён, можно командой systemd-analyze security myapp.service — она выставит оценку и подскажет, что ещё стоит включить.

Ограничения по ресурсам задаются там же: MemoryMax=512M, CPUQuota=50%. Под капотом это cgroups — подробности в статье про ограничение ресурсов через cgroups и systemd.

Запуск, изменение и диагностика

После создания или правки файла юнита обязательно перечитайте конфигурацию:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service

Не редактируйте чужие юниты напрямую — используйте drop-in: systemctl edit nginx.service создаст файл в /etc/systemd/system/nginx.service.d/override.conf, где достаточно переопределить нужные параметры. Он переживёт обновление пакета.

Полезные команды при разборе проблем:

КомандаЧто показывает
systemctl status myapp -l --no-pagerСостояние, последние строки лога, код выхода
journalctl -u myapp -fЛоги службы в реальном времени
journalctl -u myapp -b -p errТолько ошибки с момента загрузки
systemctl list-units --failedВсе упавшие юниты
systemctl cat myappИтоговый юнит со всеми drop-in
systemd-analyze blameЧто дольше всего стартовало при загрузке
systemd-analyze verify myapp.serviceПроверка синтаксиса до запуска

Тонкости фильтрации логов journald — в отдельном материале про анализ логов с journalctl.

Таймеры вместо cron

Периодические задачи в systemd описываются парой юнитов: .service с самой работой и .timer с расписанием. В отличие от cron, таймеры логируются в journald, умеют догонять пропущенные запуски (Persistent=true) и наследуют все настройки изоляции и лимитов. Разбор с примерами — в статье про systemd timers как замену cron.

Заодно совет по надёжности: если задача критична, факт её выполнения стоит контролировать снаружи — heartbeat-мониторинг заметит молчание таймера, даже если сервер жив и никаких алертов не прислал.

FAQ

Почему служба не стартует, а status показывает code=exited, status=203/EXEC? systemd не смог запустить исполняемый файл: неверный путь в ExecStart, нет прав на выполнение или указан не абсолютный путь. Относительные пути в юнитах не работают.

Почему переменные из .bashrc не видны в службе? Служба не запускает интерактивный shell. Переменные задаются через Environment= или EnvironmentFile=.

Чем enable отличается от start? start запускает службу сейчас, enable включает автозапуск при загрузке. enable --now делает и то, и другое.

Как посмотреть, что именно сейчас применяется к юниту? systemctl cat myapp покажет итоговую конфигурацию со всеми переопределениями, а systemctl show myapp — все параметры с фактическими значениями.

Опубликовано 29 октября 20259 минут чтенияАнастасия Левина
Средний рейтинг статьи — 4.8

Настроить мониторинг за 30 секунд

Надежные оповещения о даунтаймах. Без ложных срабатываний