Systemd: розширені сценарії керування сервісами
Systemd давно перестав бути просто менеджером ініціалізації. Сьогодні це повноцінна екосистема для оркестрації процесів, управління ресурсами, налагодження залежностей і моніторингу сервісів у реальному часі. Незважаючи на це, більшість адміністраторів і DevOps-інженерів використовують лише поверхневий набір можливостей - systemctl start, stop, enable - залишаючи за кадром потужні механізми, що здатні суттєво спростити роботу з production-середовищами.
У цій статті розглянемо розширені сценарії роботи з systemd: від тонкого налаштування unit-файлів і управління залежностями до використання шаблонів, socket-активації та інтеграції з cgroups. Матеріал розрахований на фахівців, які вже знайомі з базовим синтаксисом systemd і хочуть перейти на якісно інший рівень автоматизації та надійності.
Важливо розуміти, що грамотна конфігурація systemd - це не лише питання зручності. Це безпосередньо впливає на час відновлення сервісів після збоїв, споживання системних ресурсів, порядок завантаження та ізоляцію процесів. Неправильно налаштовані залежності між сервісами є однією з найпоширеніших причин тихих відмов у виробничих системах.
Стаття побудована навколо реальних кейсів: мікросервісна архітектура, фонові воркери, сокетні сервери та периодичні задачі - усе те, з чим стикаються інженери щодня.
Анатомія unit-файлу: що насправді важливо
Unit-файл складається з трьох основних секцій: [Unit], [Service] (або інший тип unit), [Install]. Проте в кожній з них є директиви, які рідко потрапляють у туторіали.
Секція [Unit]: залежності та умови
[Unit] Description=My Application Worker Documentation=https://docs.example.com After=network-online.target postgresql.service Wants=network-online.target Requires=postgresql.service BindsTo=postgresql.service
Різниця між Requires, Wants і BindsTo є критичною:
- Requires - якщо залежність не запустилася, поточний сервіс також не запуститься. Але якщо залежність зупиняється після запуску, поточний сервіс продовжує роботу.
- BindsTo - жорсткіший варіант: якщо залежність зупиниться з будь-якої причини, systemd автоматично зупинить і поточний сервіс. Ідеально для sidecar-процесів.
- Wants - м'яка залежність. Якщо залежність не запустилась, поточний сервіс усе одно буде запущений.
Директива After визначає лише порядок запуску, але не залежність. Це поширена помилка: якщо вказати тільки After=postgresql.service без Requires, сервіс запуститься навіть якщо PostgreSQL не піднявся.
Умовні директиви
Systemd підтримує набір умов, які перевіряються перед запуском:
[Unit] ConditionPathExists=/etc/myapp/config.yaml ConditionFileNotEmpty=/var/lib/myapp/db.sqlite AssertPathIsDirectory=/var/log/myapp
Різниця між Condition* і Assert*: при невиконанні умови Condition* сервіс просто пропускається (вважається успішно завершеним), тоді як Assert* генерує помилку. Це важливо для правильного відображення статусу в systemctl status.
Типи запуску та їх вплив на надійність
Type=notify та готовність сервісу
Один з найважливіших параметрів - Type. Він визначає, як systemd розуміє, що сервіс "готовий до роботи".
[Service] Type=notify ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yaml NotifyAccess=main WatchdogSec=30s
При Type=notify процес повинен надіслати systemd сигнал готовності через sd_notify(3). Це гарантує, що залежні сервіси не запустяться, поки ваш застосунок справді не готовий приймати з'єднання - а не просто запустився.
У Python-застосунку це реалізується так:
import systemd.daemon def main(): # ініціалізація застосунку db = connect_database() cache = init_cache() # повідомляємо systemd, що ми готові systemd.daemon.notify('READY=1') # основний цикл serve_requests()
Для Go використовується пакет github.com/coreos/go-systemd/daemon:
daemon.SdNotify(false, daemon.SdNotifyReady)
Watchdog: автоматичний перезапуск при зависанні
WatchdogSec=30s активує механізм watchdog. Процес зобов'язаний регулярно надсилати WATCHDOG=1 через sd_notify. Якщо він цього не робить протягом вказаного інтервалу - systemd перезапускає сервіс. Це ефективніше, ніж стандартний Restart=always, оскільки реагує на зависання, а не лише на падіння.
Шаблони unit-файлів
Одна з найпотужніших, але недооцінених можливостей - шаблонні unit-файли. Вони дозволяють запускати кілька екземплярів одного сервісу з різними параметрами.
Файл називається з символом @: /etc/systemd/system/worker@.service
[Unit] Description=Application Worker Instance %i After=network.target rabbitmq.service Requires=rabbitmq.service [Service] Type=simple User=appuser Environment=WORKER_ID=%i Environment=QUEUE_NAME=queue-%i ExecStart=/usr/local/bin/worker --id %i --queue queue-%i Restart=on-failure RestartSec=5s StandardOutput=journal StandardError=journal SyslogIdentifier=worker-%i [Install] WantedBy=multi-user.target
Запуск кількох екземплярів:
systemctl enable --now worker@1.service systemctl enable --now worker@2.service systemctl enable --now worker@3.service
Перегляд логів конкретного воркера:
journalctl -u worker@2.service -fКорисні специфікатори шаблонів:
| Специфікатор | Значення |
|---|---|
| %i | Рядок між @ і .service |
| %n | Повна назва unit |
| %H | Ім'я хоста |
| %u | Ім'я користувача (з User=) |
Socket-активація - це механізм, при якому systemd відкриває сокет і передає його застосунку лише тоді, коли надходить перше з'єднання. Це зменшує час завантаження системи та економить ресурси.
Кейс: HTTP API-сервер із socket-активацією
Файл /etc/systemd/system/myapi.socket:
[Unit] Description=My API Socket [Socket] ListenStream=8080 Accept=no [Install] WantedBy=sockets.target
Файл /etc/systemd/system/myapi.service:
[Unit] Description=My API Service Requires=myapi.socket [Service] Type=notify ExecStart=/usr/local/bin/myapi StandardInput=socket
У застосунку на Go отримання сокету від systemd:
import "github.com/coreos/go-systemd/activation" func main() { listeners, err := activation.Listeners() if err != nil || len(listeners) == 0 { // fallback: відкриваємо сокет самостійно ln, _ = net.Listen("tcp", ":8080") } else { ln = listeners[0] } http.Serve(ln, handler) }
Перевага socket-активації також у тому, що при перезапуску сервісу вхідні з'єднання не відхиляються — вони буферизуються в сокеті systemd до моменту готовності нового процесу.
Управління ресурсами через cgroups
Systemd повністю інтегрований з cgroups v2, що дозволяє встановлювати жорсткі ліміти на ресурси для кожного сервісу.
[Service] # CPU CPUQuota=50% CPUWeight=100 # Пам'ять MemoryMax=512M MemorySwapMax=0 # I/O IOWeight=50 IOReadBandwidthMax=/dev/sda 50M IOWriteBandwidthMax=/dev/sda 20M # Мережа (потребує додаткової конфігурації) IPAddressAllow=10.0.0.0/8 192.168.0.0/16 IPAddressDeny=any
MemorySwapMax=0 повністю забороняє свопування для цього сервісу - критично важлива опція для latency-sensitive застосунків.
Перевірити поточне споживання ресурсів:
systemctl status myapp.service # або детальніше: systemd-cgtop
Ізоляція та безпека
Systemd надає вбудовані механізми пісочниці, які не вимагають ні Docker, ні іншого контейнеризатора.
[Service] # Файлова система ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/lib/myapp /var/log/myapp PrivateTmp=true # Мережа PrivateNetwork=false RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # Системні виклики SystemCallFilter=@system-service SystemCallFilter=~@privileged @resources SystemCallArchitectures=native # Привілеї NoNewPrivileges=true CapabilityBoundingSet=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE # Простори імен PrivateUsers=true ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true
ProtectSystem=strict монтує /usr, /boot та /etc як read-only. У поєднанні з ReadWritePaths це фактично задає whitelist директорій, до яких сервіс має доступ на запис.
Перевірка рівня безпеки сервісу:
systemd-analyze security myapp.serviceКоманда виводить оцінку від 0 (максимальний захист) до 10 (незахищений) з переліком конкретних рекомендацій.
Таймери як заміна cron
Systemd-таймери є потужнішою альтернативою cron: вони підтримують залежності, логуються через journald і дозволяють точно контролювати поведінку при пропущених запусках.
Файл /etc/systemd/system/backup.timer:
[Unit] Description=Daily Database Backup Timer [Timer] OnCalendar=*-*-* 03:00:00 RandomizedDelaySec=600 Persistent=true AccuracySec=1min [Install] WantedBy=timers.target
Файл /etc/systemd/system/backup.service:
[Unit] Description=Database Backup After=postgresql.service Requires=postgresql.service [Service] Type=oneshot User=postgres ExecStart=/usr/local/bin/backup.sh StandardOutput=journal
Persistent=true — якщо система була вимкнена під час запланованого запуску, таймер спрацює одразу після завантаження. RandomizedDelaySec розподіляє навантаження, якщо кілька систем мають однакові таймери (актуально для fleet-адміністрування).
Перегляд усіх активних таймерів:
systemctl list-timers --allDrop-in файли: розширення без редагування оригіналу
Drop-in файли дозволяють розширювати конфігурацію systemd-unit без зміни оригінального файлу - це критично важливо при роботі з пакетними менеджерами, де оновлення пакету перезаписує unit-файл.
systemctl edit postgresql.serviceКоманда відкриває редактор і створює файл /etc/systemd/system/postgresql.service.d/override.conf. Приклад - додавання environment-змінних і лімітів ресурсів до стандартного PostgreSQL unit:
[Service] Environment=PGDATA=/data/postgresql/14/main LimitNOFILE=65536 MemoryMax=4G Restart=always RestartSec=3s
Перегляд фінальної конфігурації з урахуванням усіх drop-in:
systemctl cat postgresql.service
Практичний кейс: мікросервіс із повним набором best practices
Зведемо разом усі розглянуті концепції у реалістичний приклад - production-ready unit для Node.js API-сервісу:
[Unit] Description=Node.js Payment API Documentation=https://wiki.internal/payment-api After=network-online.target redis.service Requires=redis.service StartLimitIntervalSec=60s StartLimitBurst=3 [Service] Type=notify User=nodeapp Group=nodeapp WorkingDirectory=/opt/payment-api # Змінні середовища EnvironmentFile=/etc/payment-api/env Environment=NODE_ENV=production # Запуск та зупинка ExecStartPre=/usr/local/bin/node --check /opt/payment-api/index.js ExecStart=/usr/local/bin/node /opt/payment-api/index.js ExecReload=/bin/kill -HUP $MAINPID KillSignal=SIGTERM KillMode=mixed TimeoutStopSec=30s # Перезапуск Restart=on-failure RestartSec=5s WatchdogSec=60s # Ліміти ресурсів MemoryMax=512M MemorySwapMax=0 CPUQuota=80% LimitNOFILE=65536 # Безпека NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/log/payment-api /var/lib/payment-api PrivateTmp=true SystemCallFilter=@system-service # Логування StandardOutput=journal StandardError=journal SyslogIdentifier=payment-api [Install] WantedBy=multi-user.target
ExecStartPre виконує синтаксичну перевірку JS-файлу перед стартом - сервіс не запуститься з некоректним кодом. KillMode=mixed спочатку надсилає SIGTERM головному процесу, а потім SIGKILL усій групі процесів - важливо для Node.js з дочірніми воркерами.
Висновки
Systemd - це не просто init. Це повноцінний інструментарій для управління lifecycle сервісів, їх ізоляції та моніторингу. Грамотне використання залежностей (BindsTo, After, Requires), типів запуску (Type=notify), watchdog-механізмів і cgroups-лімітів дає змогу будувати надійні, безпечні та передбачувані production-системи.
Шаблонні unit-файли усувають дублювання конфігурацій, socket-активація мінімізує час завантаження, а drop-in override-файли забезпечують зручне розширення без конфліктів із пакетним менеджером.
Найголовніше - завжди верифікуйте конфігурацію:
systemd-analyze verify /etc/systemd/system/myapp.service systemd-analyze security myapp.service journalctl -u myapp.service --since "1 hour ago"
Ці три команди після кожної зміни unit-файлу мають стати обов'язковим рефлексом. Systemd надає всі інструменти - залишається лише ними скористатися.