ADG Оставить заявку
Блог Системное администрирование 5 мин чтения

systemd unit-файлы: пишем сервисы с ограничением ресурсов, автоперезапуском и зависимостями

Практика написания unit-файлов для собственных сервисов на RHEL 7 / CentOS 7: cgroups, Restart=, зависимости, сравнение с SysV init.

Контекст момента

systemd становится де-факто стандартом в RHEL 7, CentOS 7 и готовится к включению по умолчанию в Debian 8; сообщество разделилось в оценках

В январском посте про CentOS 7 в продакшне мы упоминали, что unit-файлы для пакетных сервисов идут из коробки, а проблемы начинаются с кастомными вещами. Пора разобрать эту тему отдельно - потому что к нам уже дважды приходили с одним и тем же запросом: «у нас Java-демон, раньше был init.d-скрипт, что делать».

Debian 8 ещё не вышел, но уже объявлено: там тоже будет systemd по умолчанию. Сообщество делится на два лагеря - одни считают systemd монолитным монстром нарушающим Unix-way, другие говорят что init.d-скрипты были адом без стандарта. Мы в этой дискуссии занимаем прагматичную позицию: на RHEL 7 / CentOS 7 systemd уже есть, и нам с ним работать.

Что не так с SysV init-скриптами

Для понимания зачем unit-файлы лучше, стоит вспомнить что именно раздражало в /etc/init.d/.

Нет стандарта на содержимое. Каждый init.d-скрипт - авторское произведение. Один разработчик добавляет --daemon, другой форкается через start-stop-daemon, третий пишет свой PID-файл в неожиданном месте. Разобрать незнакомый скрипт на продакшн-сервере в 2 ночи - то ещё удовольствие.

Нет встроенного контроля ресурсов. Хочешь ограничить память Java-процесса? Пишешь wrapper, ставишь ulimit или цепляешь cgroup руками. Каждый раз по-другому.

Нет нормального статуса и перезапуска. service foo status может возвращать «running» когда процесс завис. Автоперезапуск? Только если кто-то написал это в скрипте или поставил monit сверху.

Зависимости - комментарии, а не контракт. LSB-заголовки в # Required-Start: никто особо не проверяет при параллельном старте.

Unit-файл: базовый шаблон

Для кастомного сервиса создаём файл в /etc/systemd/system/. Вот шаблон который мы используем для Java-демонов и подобных процессов:

[Unit]
Description=My Application Service
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -jar /opt/myapp/app.jar
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Разберём ключевые моменты.

After= и Requires=. After= определяет порядок старта - наш сервис стартует после network.target и postgresql.service. Requires= означает жёсткую зависимость: если postgresql не поднялся, наш сервис тоже не стартует и будет это сообщать честно, а не молча падать через 30 секунд с криптичной ошибкой подключения.

Type=simple vs Type=forking. Это главная ловушка для тех кто переносит старые сервисы. simple - процесс остаётся на переднем плане, systemd считает его запущенным сразу. forking - systemd ждёт когда родительский процесс завершится и управление перейдёт к дочернему (классический unix-daemon). Если указать forking для процесса который не форкается - systemd подождёт, не дождётся и объявит сервис упавшим. Поймали это на стенде с одним агентом мониторинга, потеряли минут сорок.

Restart=on-failure. systemd сам перезапустит сервис если тот завершился с ненулевым кодом. RestartSec=5s - пауза перед перезапуском, чтобы не получить спираль смерти если приложение падает сразу при старте. Для критичных сервисов добавляем StartLimitBurst=5 и StartLimitInterval=60s - не более 5 перезапусков за минуту, потом systemd сдаётся и уведомляет об ошибке.

Ограничение ресурсов через cgroups

Вот где systemd реально выигрывает у SysV. Прямо в unit-файле, без дополнительных инструментов:

[Service]
MemoryLimit=512M
CPUQuota=50%

MemoryLimit=512M - если процесс превысит 512 МБ, ядро его убьёт. systemd это зафиксирует и при Restart=on-failure перезапустит. CPUQuota=50% - не более 50% одного ядра. Для фоновых задач которые не должны перегружать сервер это удобнее чем nice или ручная возня с cgroups.

Также есть LimitNOFILE= для лимита файловых дескрипторов - актуально для сетевых серверов, где стандартный ulimit часто забывают поднять и потом ловят «too many open files» в 3 часа ночи.

Посмотреть фактические cgroup-лимиты работающего сервиса:

systemctl show myapp.service | grep -i limit

После изменения unit-файла

Частая ошибка: отредактировали файл, сделали systemctl restart myapp и думаете что изменения применились. Нет. Сначала нужно:

systemctl daemon-reload
systemctl restart myapp.service

daemon-reload заставляет systemd перечитать все unit-файлы. Без него работает старая версия конфигурации. Мы добавили это в шпаргалку для команды - слишком часто люди редактировали unit, перезапускали сервис и удивлялись что ничего не поменялось.

Проверка и отладка

systemctl status myapp.service       # статус + последние строки лога
journalctl -u myapp.service -f       # живой хвост лога сервиса
journalctl -u myapp.service -n 50    # последние 50 строк
systemd-analyze verify /etc/systemd/system/myapp.service  # проверить синтаксис

systemd-analyze verify особенно полезен - он поймает опечатки в именах директив до того как вы пойдёте рестартовать сервис на продакшне.

Интеграция с Ansible

В наш провизионирующий playbook unit-файлы идут через шаблон:

- name: deploy unit file
  template:
    src: myapp.service.j2
    dest: /etc/systemd/system/myapp.service
  notify:
    - daemon-reload
    - restart myapp

Handler daemon-reload вызывает systemctl daemon-reload перед рестартом. Ansible модуль service для systemd работает корректно - понимает enabled: yes как systemctl enable.

Что имеем в итоге

Три кастомных сервиса которые раньше жили на init.d-скриптах с monit для перезапуска - теперь unit-файлы по 20-30 строк без внешних зависимостей. Ограничение памяти встроено, перезапуск встроен, зависимости декларативны. systemctl status даёт реальный статус, а не «процесс по имени найден, значит running».

Порог входа чуть выше чем скопировать init.d-шаблон, но результат управляемее. Сообщество пусть спорит о философии - мы пишем unit-файлы.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.