Systemd на максимум: таймеры, sandboxing, portable services

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

Компания «ЛинуксОпс» — 80 серверов, 200+ cron-задач. Проблемы с cron:

  • Нет логирования — вывод cron теряется (если не настроен MAILTO)
  • Нет зависимостей — cron не знает, что задача A должна завершиться перед задачей B
  • Нет retry — если задача упала, cron не перезапустит её
  • Нет мониторинга — невозможно узнать, работает ли задача прямо сейчас

Systemd таймеры решают все четыре проблемы и полностью заменяют cron.

# /etc/systemd/system/backup-db.service
[Unit]
Description=Database backup
After=postgresql.service

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup-db.sh
User=postgres
Nice=10
IOSchedulingClass=idle

# /etc/systemd/system/backup-db.timer
[Unit]
Description=Daily database backup at 3:00 AM

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

[Install]
WantedBy=timers.target
# Активация
systemctl enable --now backup-db.timer

# Список всех таймеров
systemctl list-timers --all

# Статус и логи
systemctl status backup-db.timer
systemctl status backup-db.service
journalctl -u backup-db.service --since today

# Запуск вручную (для тестирования)
systemctl start backup-db.service
Совет: Persistent=true — если сервер был выключен во время запланированного запуска, таймер сработает при следующей загрузке. Cron такое не умеет (кроме anacron).

Sandboxing сервисов: ProtectSystem, PrivateTmp

Systemd предоставляет 30+ директив для изоляции сервисов — без Docker, без SELinux, встроенными средствами.

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target

[Service]
ExecStart=/opt/myapp/bin/server
User=myapp
Group=myapp

# === FILESYSTEM ISOLATION ===
ProtectSystem=strict         # /usr, /boot, /efi — read-only
ProtectHome=true             # /home, /root, /run/user — недоступны
PrivateTmp=true              # Изолированный /tmp
ReadWritePaths=/var/lib/myapp /var/log/myapp

# === NETWORK ISOLATION ===
PrivateNetwork=false         # true = нет сети вообще
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

# === PRIVILEGE ISOLATION ===
NoNewPrivileges=true         # Запрет escalation
CapabilityBoundingSet=       # Никаких capabilities
ProtectKernelTunables=true   # /proc/sys — read-only
ProtectKernelModules=true    # Нельзя загружать модули ядра
ProtectControlGroups=true    # /sys/fs/cgroup — read-only

# === SYSTEM CALL FILTER ===
SystemCallFilter=@system-service  # Только безопасные syscalls
SystemCallArchitectures=native    # Только родная архитектура

# === MISC ===
MemoryDenyWriteExecute=true  # W^X — нет JIT/самомодификации
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true

[Install]
WantedBy=multi-user.target
# Проверка уровня sandboxing
systemd-analyze security myapp.service
# myapp.service: 1.8 SAFE 🟢
#   PrivateNetwork=     no    0.5
#   ProtectSystem=      strict 0.0
#   PrivateTmp=         yes   0.0
#   ... (полный список)

Journal: продвинутая фильтрация и экспорт

Journald хранит логи в бинарном формате с индексами — поиск работает мгновенно даже на гигабайтах логов.

# Логи конкретного сервиса за последний час
journalctl -u nginx --since "1 hour ago"

# Логи по приоритету (только ошибки)
journalctl -p err --since today

# Логи конкретного PID
journalctl _PID=12345

# Логи конкретного пользователя
journalctl _UID=1000

# Вывод в JSON для парсинга
journalctl -u myapp -o json --since "5 min ago" | jq '.MESSAGE'

# Логи загрузки (текущая и предыдущая)
journalctl -b 0    # текущая загрузка
journalctl -b -1   # предыдущая загрузка

# Размер журнала
journalctl --disk-usage
# Archived and active journals take up 2.4G

# Ограничение размера
# /etc/systemd/journald.conf
# SystemMaxUse=1G
# MaxRetentionSec=30day

Экспорт логов в Loki (Grafana):

# Promtail конфигурация для чтения journal
# /etc/promtail/config.yml
scrape_configs:
  - job_name: journal
    journal:
      max_age: 12h
      labels:
        job: systemd-journal
    relabel_configs:
      - source_labels: ['__journal__systemd_unit']
        target_label: 'unit'
      - source_labels: ['__journal_priority_keyword']
        target_label: 'level'

Portable Services: контейнеры без контейнеров

Portable Services — механизм systemd для запуска изолированных сервисов из образов (без Docker/Podman). Образ содержит приложение + зависимости, systemd монтирует его через overlay.

# Создание portable service image
mkdir -p myapp-portable/usr/lib/systemd/system
mkdir -p myapp-portable/opt/myapp

# Копируем приложение и зависимости
cp -a /opt/myapp/* myapp-portable/opt/myapp/

# Создаём unit-файл внутри образа
cat > myapp-portable/usr/lib/systemd/system/myapp.service <<EOF
[Unit]
Description=My Portable App

[Service]
ExecStart=/opt/myapp/bin/server
DynamicUser=yes
StateDirectory=myapp
EOF

# Собираем образ (squashfs)
mksquashfs myapp-portable myapp_1.0.raw -comp zstd

# Подключаем portable service
portablectl attach myapp_1.0.raw
systemctl start myapp.service
systemctl status myapp.service

Преимущества перед Docker для простых сервисов:

  • Нативная интеграция с systemd (логи, зависимости, sandboxing)
  • Нет overhead контейнерной runtime
  • Работает на серверах без Docker/Podman
  • Образ — один файл, легко копировать через scp

Результаты «ЛинуксОпс»: миграция cron → systemd

Миграция 200 cron-задач на 80 серверах:

# Скрипт конвертации cron → systemd timer
# Для каждой строки crontab:
# 30 3 * * * /opt/scripts/cleanup.sh
# Генерируем:
# cleanup.service + cleanup.timer с OnCalendar=*-*-* 03:30:00
МетрикаCronSystemd timers
ЛогированиеMAILTO (если настроен)journalctl -u name
Мониторинг выполненияНетsystemctl list-timers
Retry при ошибкеНетRestart=on-failure
ЗависимостиНетAfter=, Requires=
Ограничение ресурсовНет (nice, ionice вручную)CPUQuota=, MemoryMax=
SandboxingНетProtectSystem=, PrivateTmp=
Пропущенные запускиПотеряныPersistent=true
Совет: Не удаляйте cron сразу. Запустите таймеры параллельно с cron на 1-2 недели, убедитесь что всё работает, затем отключите cron-задачи.

Часто задаваемые вопросы

Для 95% задач — да. Systemd таймеры лучше во всём: логирование, мониторинг, зависимости, sandboxing, retry. Cron остаётся удобнее для быстрых одноразовых задач (одна строка vs два файла). Для production-задач systemd таймеры предпочтительнее.

Директива RandomizedDelaySec=600 добавит случайную задержку от 0 до 600 секунд. Это полезно когда 80 серверов запускают одну и ту же задачу одновременно — рандомизация предотвращает thundering herd.

ProtectSystem=strict делает всю файловую систему read-only кроме /dev, /proc, /sys. Добавьте ReadWritePaths= для директорий, куда приложение пишет: ReadWritePaths=/var/lib/myapp /var/log/myapp /tmp/myapp. Или используйте ProtectSystem=full (более мягкий — только /usr и /boot read-only).

В секции [Service]: MemoryMax=512M (жёсткий лимит, OOM kill при превышении), MemoryHigh=384M (мягкий лимит, throttling), CPUQuota=50% (не больше половины одного ядра). Для проверки: systemctl show myapp.service -p MemoryMax,CPUQuota.

Journald — по умолчанию в systemd, структурированные бинарные логи, мгновенный поиск по индексам. Rsyslog — текстовые файлы, совместимость со старыми инструментами. Рекомендация: journald как основной + пересылка в Loki/ELK для долгосрочного хранения. Rsyslog нужен, если есть legacy-инструменты, парсящие /var/log/syslog.

Нужна помощь с внедрением?

Настроим, оптимизируем и возьмём на поддержку вашу инфраструктуру. 15+ лет опыта, 8 серверов Dell Xeon в дата-центре МТС.

📞 Связаться с нами

Комментарии 0

Оставить комментарий

3 + 7 =