Systemd — это уже не просто init-система. Это целая экосистема для управления сервисами, логами, сетью, таймерами, точками монтирования и многим другим. Большинство администраторов использует 20% его возможностей, не подозревая об остальных 80%. Эта статья закроет пробелы.
Архитектура systemd: что за чем стоит
systemd
├── systemd-journald — централизованное логирование
├── systemd-networkd — управление сетью
├── systemd-resolved — DNS resolver
├── systemd-timesyncd — синхронизация времени
├── systemd-logind — управление сессиями
├── systemd-udevd — управление устройствами
└── systemd-tmpfilesd — управление временными файлами
Всё взаимодействует через D-Bus и сокеты. Это важно понимать для отладки.
Unit-файлы: анатомия сервиса
Базовая структура
[Unit]
Description=MyApp Web Service
Documentation=https://myapp.example.com/docs
After=network.target mysql.service redis.service
Requires=mysql.service
Wants=redis.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/php /var/www/myapp/server.php
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
Секция [Unit]: зависимости и порядок
Разница между After/Before и Requires/Wants критически важна:
# ПОРЯДОК ЗАПУСКА (не зависимость!)
After=network.target # запускаемся после network.target
Before=nginx.service # запускаемся до nginx
# ЗАВИСИМОСТИ (и порядок)
Requires=mysql.service # жёсткая: если mysql упал — мы тоже падаем
Wants=redis.service # мягкая: пробуем запустить redis, но не критично
BindsTo=device.mount # как Requires, но реагирует на остановку устройства
PartOf=app.service # stop/restart parent = stop/restart нас
# Конфликты
Conflicts=apache2.service # не запускаться вместе с apache2
Типы сервисов — Type=
Type=simple # процесс запущен = сервис готов (дефолт)
Type=exec # как simple, но ждёт exec() (systemd 240+)
Type=forking # главный процесс форкает дочерний и завершается (nginx, apache)
Type=oneshot # выполняется и завершается (для скриптов)
Type=notify # процесс сигнализирует systemd о готовности через sd_notify()
Type=dbus # готовность через D-Bus
Type=idle # запускается после других, когда нет других задач
Для PHP-FPM используем Type=notify — он поддерживает sd_notify:
[Service]
Type=notify
ExecStart=/usr/sbin/php-fpm8.2 --nodaemonize
Продвинутые настройки сервиса
Переменные окружения и секреты
[Service]
# Прямо в unit-файле (не для секретов!)
Environment=APP_ENV=production
Environment=LOG_LEVEL=info
# Из файла (безопаснее)
EnvironmentFile=/etc/myapp/environment
EnvironmentFile=-/etc/myapp/local.env # дефис = не ошибка если нет файла
# Systemd credentials (systemd 250+ — самый безопасный способ)
LoadCredential=db-password:/etc/credstore/db-password.cred
# Доступно в: /run/credentials/myapp.service/db-password
Файл /etc/myapp/environment:
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp
DB_USER=myapp
DB_PASSWORD=supersecret
REDIS_HOST=127.0.0.1
chmod 600 /etc/myapp/environment
chown root:www-data /etc/myapp/environment
Изоляция и безопасность сервиса
[Service]
# Запуск от непривилегированного пользователя
User=www-data
Group=www-data
# Динамический пользователь (создаётся на время жизни сервиса)
DynamicUser=yes
# Файловая система
PrivateTmp=yes # отдельный /tmp
PrivateDevices=yes # нет доступа к /dev (кроме базовых)
ProtectHome=yes # нет доступа к /home /root /run/user
ProtectSystem=strict # / только для чтения
ReadWritePaths=/var/www/myapp/storage /var/log/myapp
# Сеть
PrivateNetwork=no # нужна для веб-сервиса
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# Процессы
NoNewPrivileges=yes # нет setuid/setgid после старта
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
# Системные вызовы (белый список)
SystemCallFilter=@system-service # разрешённые для сервисов
SystemCallFilter=~@privileged # запрещаем привилегированные
SystemCallErrorNumber=EPERM
# Capabilities
CapabilityBoundingSet=CAP_NET_BIND_SERVICE # только для порта <1024
AmbientCapabilities=CAP_NET_BIND_SERVICE
# Ограничение ресурсов
LimitNOFILE=65535 # открытые файловые дескрипторы
LimitNPROC=512 # процессы
MemoryLimit=2G # максимум памяти
CPUQuota=50% # не более 50% CPU
Политики перезапуска
[Service]
Restart=on-failure # только при ненулевом коде возврата
Restart=always # всегда (кроме systemctl stop)
Restart=unless-stopped # всегда, даже при ненулевом коде
RestartSec=5s # пауза перед перезапуском
# Ограничение перезапусков (не более 3 за 30 секунд)
StartLimitIntervalSec=30s
StartLimitBurst=3
StartLimitAction=none # что делать при достижении лимита: none|reboot|poweroff
# Watchdog — если нет heartbeat за N секунд — перезапуск
WatchdogSec=30s
systemctl: продвинутое использование
# Основные команды
systemctl start myapp
systemctl stop myapp
systemctl restart myapp
systemctl reload myapp # отправить SIGHUP (graceful reload)
systemctl status myapp
# Включить/выключить автозапуск
systemctl enable myapp
systemctl disable myapp
systemctl enable --now myapp # включить и сразу запустить
# Маскировка (нельзя запустить даже вручную)
systemctl mask cups
systemctl unmask cups
# Информация о юните
systemctl cat myapp # показать содержимое unit-файла
systemctl show myapp # все свойства (машиночитаемо)
systemctl show myapp -p Restart,RestartSec # конкретные свойства
# Зависимости
systemctl list-dependencies myapp
systemctl list-dependencies --reverse myapp # кто зависит от нас
# Все юниты
systemctl list-units --type=service
systemctl list-units --state=failed
systemctl list-unit-files
# Применение изменений без потери сервиса (systemd 230+)
systemctl daemon-reload
systemctl try-reload-or-restart myapp
systemd Timers: замена cron
Таймеры systemd мощнее cron: есть логирование, управление зависимостями, можно запустить вручную.
Создаём пару service + timer
/etc/systemd/system/backup.service:
[Unit]
Description=Daily Database Backup
After=network.target mysql.service
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup-database.sh
StandardOutput=journal
StandardError=journal
/etc/systemd/system/backup.timer:
[Unit]
Description=Daily Database Backup Timer
Requires=backup.service
[Timer]
# Каждый день в 2:30
OnCalendar=*-*-* 02:30:00
# После загрузки системы (если пропустили)
Persistent=true
# Случайная задержка до 5 минут (чтобы не все запускались одновременно)
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
systemctl enable --now backup.timer
systemctl list-timers # все таймеры и когда запустятся
systemctl start backup.service # запустить вручную без ожидания таймера
Синтаксис OnCalendar
OnCalendar=Mon-Fri *-*-* 09:00:00 # каждый будний день в 9:00
OnCalendar=weekly # каждую неделю (понедельник 0:00)
OnCalendar=monthly # первый день месяца
OnCalendar=*-*-* *:00/15:00 # каждые 15 минут
OnCalendar=2024-03-* # каждый день в марте 2024
OnCalendar=Sat,Sun 12:00:00 # субботу и воскресенье в полдень
Проверка синтаксиса:
systemd-analyze calendar "Mon-Fri *-*-* 09:00:00"
journald: работа с логами
Базовые команды
# Логи конкретного сервиса
journalctl -u nginx
journalctl -u nginx -n 100 # последние 100 строк
journalctl -u nginx -f # следим в реальном времени (как tail -f)
# По времени
journalctl --since "2024-01-15 10:00" --until "2024-01-15 11:00"
journalctl --since "1 hour ago"
journalctl --since yesterday
# По приоритету
journalctl -p err # только ошибки
journalctl -p warning..err # от warning до err
# 0=emerg 1=alert 2=crit 3=err 4=warning 5=notice 6=info 7=debug
# Форматы вывода
journalctl -u myapp -o json # JSON
journalctl -u myapp -o json-pretty # читаемый JSON
journalctl -u myapp -o short-precise # с микросекундами
# Загрузки системы
journalctl --list-boots
journalctl -b 0 # текущая загрузка
journalctl -b -1 # предыдущая загрузка
journalctl -b -1 -p err # ошибки при предыдущей загрузке
Настройка journald
/etc/systemd/journald.conf:
[Journal]
# Максимальный размер хранилища
SystemMaxUse=2G
SystemKeepFree=500M
# Максимальный размер одного файла журнала
SystemMaxFileSize=200M
# Хранить журналы N дней
MaxRetentionSec=1month
# Сжатие
Compress=yes
# Пересылка в syslog (если нужен rsyslog)
ForwardToSyslog=no
ForwardToWall=no
# Максимальный размер сообщения
LineMax=48K
Структурированное логирование из приложения
# Systemd-cat
echo "Application started" | systemd-cat -t myapp -p info
# С полями для фильтрации
systemd-cat -t myapp <<< "MESSAGE=User login SUCCESSFUL
USER_ID=42
REMOTE_IP=192.168.1.100"
# Фильтрация по custom полям
journalctl SYSLOG_IDENTIFIER=myapp
journalctl _SYSTEMD_UNIT=myapp.service USER_ID=42
Drop-in конфиги: расширяем без изменения оригинала
Никогда не редактируйте файлы в /lib/systemd/system/ — они перезапишутся при обновлении. Используйте drop-in:
# Создаём директорию
mkdir -p /etc/systemd/system/nginx.service.d/
# Создаём override
cat > /etc/systemd/system/nginx.service.d/override.conf <<EOF
[Service]
# Добавляем переменные окружения к nginx
Environment=APP_ENV=production
# Увеличиваем лимит файловых дескрипторов
LimitNOFILE=65535
# Рестарт при падении (дефолт nginx — no)
Restart=on-failure
RestartSec=5s
EOF
# Или через systemctl edit (открывает редактор автоматически)
systemctl edit nginx
systemctl daemon-reload
cgroups v2 через systemd
Современные версии systemd используют cgroups v2 для изоляции ресурсов:
# Статистика ресурсов для сервиса
systemctl status myapp
# или детальнее
systemd-cgtop
# Запустить с ограничениями на лету (transient unit)
systemd-run --unit=my-temp-task --slice=user.slice \
-p MemoryMax=512M -p CPUQuota=25% \
/usr/bin/python3 heavy_script.py
# Ограничить группу пользователя
systemctl set-property user-1000.slice MemoryMax=4G
Анализ времени загрузки
# Общее время загрузки
systemd-analyze
# Детально по сервисам
systemd-analyze blame
# Визуализация в SVG
systemd-analyze plot > boot.svg
# Критический путь загрузки
systemd-analyze critical-chain
# Проверка unit-файла на ошибки
systemd-analyze verify /etc/systemd/system/myapp.service
Полезные паттерны для PHP-приложений
Сервис PHP-FPM с worker isolation
[Service]
# Мягкий рестарт (дождаться завершения текущих запросов)
ExecReload=/bin/kill -USR2 $MAINPID
# Watchdog через sd_notify
WatchdogSec=60s
NotifyAccess=main
# Логи в journald напрямую
StandardOutput=journal
StandardError=journal
SyslogIdentifier=php-fpm
# Автоматический сбор core dump
LimitCORE=infinity
Slice=web.slice # группировка в slice для общих лимитов
Slice для группировки сервисов
# /etc/systemd/system/web.slice
[Unit]
Description=Web Services Slice
Before=slices.target
[Slice]
MemoryMax=8G
CPUQuota=200%
Все сервисы в web.slice суммарно не превысят 8GB RAM и 200% CPU (2 ядра).
Отладка: когда что-то пошло не так
# Быстрая диагностика
systemctl --failed # упавшие сервисы
journalctl -p err -b # ошибки с последней загрузки
journalctl -xe # последние записи с расшифровкой
# Подробный запуск в debug-режиме
SYSTEMD_LOG_LEVEL=debug systemd-analyze verify myapp.service
# Проверка прав и доступов
systemd-run --unit=debug-shell -p PrivateTmp=yes bash
# Запускает оболочку с теми же ограничениями, что и сервис
# Strace сервиса через systemd
systemctl set-environment STRACE_OPTS="-f -e trace=network"
systemctl restart myapp
Systemd — мощный инструмент, который при правильном использовании превращает управление сервисами из искусства в инженерию. Используйте drop-in конфиги, настраивайте изоляцию, пишите таймеры вместо cron — и ваша система станет предсказуемой и управляемой.
Create an account or sign in to leave a review
There are no reviews to display.