Systemd: сервисы, таймеры и журнал — полное руководство

Systemd стоит на подавляющем большинстве серверов, и умение с ним работать — обязательный навык для любого, кто администрирует Linux. В этой статье разберём unit-файлы, типы сервисов, таймеры, журнал, ресурсные лимиты и hardening.
Анатомия unit-файла
Unit-файл состоит из трёх секций: [Unit] (метаданные и зависимости), [Service] (параметры процесса) и [Install] (интеграция с загрузкой).
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application Server
After=network.target postgresql.service
Wants=postgresql.service
ConditionPathExists=/etc/myapp/config.yml
[Service]
Type=exec
User=app
Group=app
WorkingDirectory=/opt/myapp
Environment=NODE_ENV=production
EnvironmentFile=/etc/myapp/env
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yml
Restart=on-failure
RestartSec=5s
TimeoutStartSec=30s
TimeoutStopSec=30s
[Install]
WantedBy=multi-user.target Важное различие зависимостей: After задаёт порядок запуска, Requires — жёсткую зависимость (если зависимость упала, юнит тоже остановится), Wants — мягкую (продолжит работать). На практике чаще всего нужна комбинация After + Wants.
Типы сервисов: Type=
Тип определяет, как systemd понимает, что процесс запустился. Неправильный тип приводит к таймаутам и ложным перезапускам.
Type=exec (рекомендуется) — systemd считает сервис запущенным после успешного вызова exec(). Если бинарник не найден — сразу failed.
Type=simple (по умолчанию) — считает запущенным сразу после вызова ExecStart. Не знает, готово ли приложение реально.
Type=forking — для классических Unix-демонов, которые форкаются. Требует PIDFile.
Type=oneshot — для задач, которые выполняются и завершаются. Можно указать несколько ExecStart — они выполнятся последовательно. Используйте с RemainAfterExit=yes.
Type=notify — приложение само сообщает о готовности через sd_notify. Самый надёжный вариант, но требует поддержки от приложения.
Политики перезапуска
[Service]
# on-failure — при ненулевом exit code, сигнале или таймауте
# always — при любом завершении
# on-abnormal — при сигнале или таймауте (не при exit code)
Restart=on-failure
RestartSec=5s
# Защита от бесконечного цикла: не более 3 раз за 60 секунд
StartLimitBurst=3
StartLimitIntervalSec=60s
# Коды выхода, которые НЕ считаются ошибкой
SuccessExitStatus=143 Если сервис падает чаще лимита — что-то серьёзно сломано, и бесконечные перезапуски только усугубят ситуацию.
Лимиты ресурсов
Один прожорливый процесс не должен класть весь сервер:
[Service]
MemoryHigh=400M # мягкий лимит (агрессивный своп)
MemoryMax=512M # жёсткий лимит (OOM kill)
CPUQuota=200% # два ядра (50% = пол-ядра)
TasksMax=100 # защита от fork-бомбы
LimitNOFILE=65535 # лимит открытых файлов Проверить потребление:
systemctl show myapp.service | grep -E "Memory|CPU|Tasks"
systemd-cgtop Таймеры вместо cron
Systemd timers дают логирование через journal, зависимости и обработку пропущенных запусков. Таймер — это два файла: .timer (расписание) и .service (задача).
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300
AccuracySec=1s
[Install]
WantedBy=timers.target # /etc/systemd/system/backup.service
[Unit]
Description=Daily backup job
[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup.sh
MemoryMax=256M
CPUQuota=50%
Nice=19
IOSchedulingClass=idle sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer Форматы расписания OnCalendar
OnCalendar=daily # каждый день в полночь
OnCalendar=hourly # каждый час
OnCalendar=Mon *-*-* 09:00:00 # каждый понедельник в 09:00
OnCalendar=*:0/15 # каждые 15 минут
OnCalendar=Mon..Fri *-*-* 08:30:00 # будние дни в 08:30 Для монотонных интервалов (не привязанных к календарю):
OnBootSec=15min # через 15 минут после загрузки
OnUnitActiveSec=5min # через 5 минут после последнего срабатывания Проверить расписание:
systemd-analyze calendar "Mon..Fri *-*-* 08:30:00"
systemctl list-timers --all 
Journalctl: работа с журналом
journalctl -u myapp.service # логи сервиса
journalctl -u myapp.service -f # в реальном времени
journalctl -u myapp.service -n 100 # последние 100 строк
journalctl -u myapp.service --since today
journalctl -u myapp.service -p err # только ошибки
journalctl -u myapp.service -o json-pretty -n 5 # JSON-формат
journalctl -k -p err # ошибки ядра Управление хранением:
journalctl --disk-usage # размер журнала
sudo journalctl --vacuum-size=500M # оставить 500 МБ
sudo journalctl --vacuum-time=2weeks # удалить старше двух недель Настройка в /etc/systemd/journald.conf:
[Journal]
SystemMaxUse=1G
Storage=persistent
Compress=yes
MaxRetentionSec=30days Socket activation
Systemd слушает сокет и запускает сервис только при первом запросе. Это экономит ресурсы и позволяет обновлять сервис без потери соединений:
# /etc/systemd/system/myapp.socket
[Unit]
Description=My App Socket
[Socket]
ListenStream=8080
[Install]
WantedBy=sockets.target Hardening: безопасность сервисов
Каждый сервис должен иметь минимально необходимые права:
[Service]
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myapp /var/log/myapp
PrivateDevices=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service Оценить уровень безопасности юнита:
systemd-analyze security myapp.service
# Оценка от 0 (полностью изолирован) до 10 (без изоляции)
# Цельтесь в значение до 3 Отладка проблем
systemctl status myapp.service
systemctl cat myapp.service
systemctl list-dependencies myapp.service
systemd-analyze blame # время загрузки каждого юнита Заключение
Три главных правила: всегда указывайте правильный Type= (это решает 90% проблем с запуском); используйте hardening-директивы NoNewPrivileges, ProtectSystem, PrivateTmp в каждом unit-файле; замените cron на systemd timers и получите логирование, зависимости и обработку пропущенных запусков бесплатно.