Systemd: розширені сценарії керування сервісами

event 08.03.2026 22:00
| category DevOps | person iron_will | comment 0 | visibility 156 | |

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-активація: запуск за вимогою

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 --all

Drop-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 надає всі інструменти - залишається лише ними скористатися.

Схожі пости

Robocopy: можливості та переваги перед звичайним Copy/Xcopy

Вступ У корпоративному середовищі Windows-копіювання файлів давно вийшло за межі простого перенесення даних з однієї директорії в іншу. Коли йдеться про великі обсяги інформації, мережеві шляхи, нестабільні з’єднання або вимоги до надійності, станда...

category DevOps person iron_will event 28/05/2026

VPN, SSH та базова безпека інфраструктури

Вступ Сучасна IT-інфраструктура функціонує в умовах постійного зовнішнього впливу: сканування портів, автоматизовані брутфорс-атаки, експлуатація вразливостей сервісів та цільові кібератаки. Навіть невеликі системи без належного захисту можуть стати...

category Security person iron_will event 26/05/2026

Автоматизація деплою через GitHub Actions

Вступ Сучасна розробка програмного забезпечення неможлива без автоматизації процесів доставки коду. Ручний деплой давно став вузьким місцем у життєвому циклі продукту: він збільшує ризик помилок, уповільнює релізи та ускладнює масштабування командно...

category DevOps person iron_will event 26/05/2026

Kubernetes для новачків: базові концепції

Вступ Сучасна розробка програмного забезпечення дедалі більше орієнтується на мікросервісну архітектуру, контейнеризацію та автоматизацію інфраструктури. У центрі цієї трансформації знаходиться Kubernetes - одна з найпопулярніших платформ оркестраці...

category Kubernetes person iron_will event 17/05/2026

Що таке RAID: рівні RAID, принцип роботи та навіщо він потрібен

Вступ У сучасній ІТ-інфраструктурі дані є одним із найцінніших ресурсів. Сервери, системи віртуалізації, бази даних, файлові сховища та резервні копії постійно працюють із великими обсягами інформації. Втрата даних через збій накопичувача може призв...

category System administration person iron_will event 10/05/2026

Docker: як оптимізувати розмір контейнера з 50 ГБ до керованого рівня

Вступ Контейнери давно стали стандартом де-факто для доставки застосунків у production. Проте з ростом складності систем часто виникає нетривіальна проблема - неконтрольоване збільшення розміру Docker-образів. Сценарій, коли образ досягає 30–50 ГБ,...

category DevOps person iron_will event 06/05/2026
cookie
Цей сайт використовує cookies для покращення роботи. Детальніше