среда, 15 марта 2017 г.

Systemd: пишем собственные .service и .target


У меня появился Linux на домашнем компьютере, и я поспешил обжиться в новой ОС. Она была установлена с systemd init process. Это было мое первое знакомство с этим новым инструментом. Cвой ноутбук я использую для каждодневной жизни и для программирования. Мне хотелось включать рабочие программы (Apache2 и MySQL) только на время, пока я их использую, чтобы не тратить впустую ресурсы своего компьютера. Дополнительно, для тестирования я написал bash скрипт, который выгружает содержимое одной из MySQL БД c жесткого диска в ОЗУ (в tmpfs) – так тесты выполняются значительно быстрее. По идее, я мог бы начинать свой рабочий день вот так:
systemctl start apache2.service
systemctl start mysqld.service
/root/scripts/mysqld-tmpfs start

И заканчивать его:
systemctl stop apache2.service
systemctl stop mysqld.service
/root/scripts/mysqld-tmpfs stop

Но мне хотелось сделать вещи “как надо”.

Чего я хотел?


Я хотел достичь 2 целей:
  1. Мне было лень писать 2 команды (запуск apache и запуск mysql), т.к. я знал, что обе программы всегда будут выключаться и включаться синхронно. Хотелось выполнять эту операцию одной командой.
  2. Дело попахивало неприятностями, если компьютер перезагрузится пока моя база данных будет сидеть в tmpfs – все файлы будут потеряны. Конечно, я делал бекапы, но мне опять же было лень восстанавливать их вручную после каждой непредвиденной перезагрузки.


Что я сделал?


В итоге я объединил Apache2 и MySQL в один target. Это позволило запускать оба сервиса одной командой. А свой mysqld-tmpfs скрипт я декларировал в виде сервиса в глазах systemd. Будучи сервисом, я уверен, что systemd выполнит его корректную остановку, если система пойдет на перезагрузку или еще в какую-то нештатную ситуацию, и моя БД без потерь сохранится на жесткий диск.

Что такое service?


Это некоторая программа, которая выполняется в фоне и предоставляет полезную функциональность. К примеру, Apache веб сервер. Сервисы можно запускать и останавливать. Некоторые сервисы могут запускаться и останавливаться автоматически по определенным событиям (загрузка ОС, выгрузка ОС и тп). Так же их можно запускать/останавливать вручную. Сервис декларируется в /etc/systemd/system/my-name.service файлах (с суффиксом “.service”).

Что такое target?


Target в systemd очень похож на runlevel в openRC, но это все-таки разные вещи. Во-первых, target позволяет группировать 1 и более сервисов в единый блок. Группируя сервисы в targets, ими проще управлять. Во-вторых, systemd автоматически включает/выключает targets по событиям. “Включение” target означает включение всех сервисов, которые он объединяет в себе. К примеру, если в systemd настроен target по умолчанию my-favorite.target, то при загрузке системы systemd включит все сервисы, которые задекларированы внутри my-favorite.target. В какой-то момент в консоли можно набрать:
systemctl isolate my-another.target

Все сервисы из my-another.target будут включены, и все включенные сервисы не из my-another.target будут выключены. Это очень похоже на переключение runlevel в openRC. Однако, systemd поддерживает включение более чем 1 target. Вот пример:
# Эксклюзивно включаем my-favorite.target и выключаем все остальные сервисы
systemctl isolate my-favorite.target
# К уже запущенным сервисам и targets добавляем еще 1 target
systemctl start my-another.target

После выполнения этих команд в системе будет работать объединение сервисов из my-favorite.target и my-another.target.

Как я это сделал?


В итоге у меня получился вот такой mysqld-tmpfs.service файл:
Description=Mount a MySQL database into tmpfs.
# Мой /root/scripts/tmpfs скрипт может работать как при включенном, так и при выключенном mysql сервисе. Но если бы mysql сервис нужен был включенным, к примеру, то я бы добавил эти строки:
#Requires=mysqld.service
#After=mysqld.service

[Service]
# Даем знать systemd, что этот сервис представляет из себя лишь 1 процесс. Man page хорошо описывает доступные опции.
Type=oneshot
# Выполнить эту команду при запуске сервиса.
ExecStart=/root/scripts/mysqld-tmpfs start
# Выполнить эту команду при остановке сервиса.
ExecStop=/root/scripts/mysqld-tmpfs stop
# Даем знать systemd, что сервис нужно считать запущенным, даже если основной процесс прекратил свою работу. Как раз то, что мне нужно: мой процесс выполнит монтировку и после этого прекратит свою работу, но должен считаться активным, т.к. монтировка осталась в системе.
RemainAfterExit=yes

И вот такой programming.target файл:
[Unit]
Description=Working/Programming target
Requires=mysqld.service
Requires=apache2.service
# Сюда я могу дописывать новые сервисы “Requires=another.service”, если они мне понадобятся в повседневной работе.


Какие были проблемы?


При остановке programming.target почему-то нижележащие apache2.service и mysqld.service не останавливались. Почитав как следует man page, я нашел проблему: systemd останавливает сервисы “лениво” — если никто не требует запущенный сервис, и он не был запущен явным образом, а как зависимость для какого-то другого сервиса, то systemd остановит его только при одном из 3 обстоятельств:
  1. Запустится какой-то другой сервис, который в своей декларации указывает, что он конфликтует с нашим сервисом.
  2. Выполнится systemctl isolate some-another.target или systemctl stop this.service.
  3. Наш сервис может запросить в своей декларации останавливать себя не ленивым образом, а активным, добавив вот такую строку в [Unit] секцию: StopWhenUnneeded=true


Декларации “чужих” сервисов можно менять создавая файлы /etc/systemd/system/name-i-alter.service.d/*.conf. Я просто создал /etc/systemd/system/apache2.service/auto-stop.conf и /etc/systemd/system/mysqld.service.d/auto-stop.conf и поместил туда ту строку.

Другая проблема, на которую я, наткнулся была в том, что systemd не очень любит symlinks. Я не большой любитель “загаживать” системные директории типа /etc, /bin, /usr своими локальными продуктами жизнедеятельности, поэтому изначально я попытался свой /etc/systemd/system/mysqld-tmpfs.service сделать symlink на /root/scripts/mysqld-tmpfs.service файл, т.е. хранить сам файл в домашнем каталоге root пользователя. Но systemctl команда отказывалась работать с таким сервисом выдавая малопонятные ошибки. Оказалось, что определенную часть своей внутренней кухни systemd делает именно на symlinks, и ему тогда “трудно” отличать внутреннюю кухню (свои symlinks) от сторонних *.service файлов (если они тоже являются symlinks). Удалив symlink из /etc/systemd/system/mysqld-tmpfs.service и скопировав туда содержимое настоящего файла, я решил эту проблему. Более подробное описание этой проблемы можно прочитать тут: bugzilla.redhat.com/show_bug.cgi?id=955379

Результат


Я достиг своей цели. Начиная рабочий день:
systemctl start programming.target

Когда нужно выполнить тесты на своем проекте:
systemctl start mysqld-tmpfs.service

Когда я хочу демонтировать БД из tmpfs в жесткий диск (хотя на практике я так почти не делаю, а просто оставляю БД в tmpfs на целый день, и при выключении systemd за меня запускает демонтировку из tmpfs в жесткий диск):
systemctl stop mysqld-tmpfs.service

Когда я закончил работать и хочу остановить рабочие программы:
systemctl stop programming.target


Cheat sheet


Некоторые полезности при работе с systemd:
  • Вызывайте systemctl daemon-reload, если вы изменили декларацию чего-либо (systemd считает файлы декларации заново)
  • systemctl start my-name.(service|target) – запуск сервиса или target
  • systemctl stop my-name.(service|target) – остановка сервиса или target
  • systemctl enable my-name.service – сервисы могут декларировать при каких включенных targets они должны включаться. Для этого используется [Install] секция в файле декларации сервиса. Вы, как сисадмин, имеете власть на установку этого “пожелания” сервиса. Часто сервисы “устанавливаются” в target по умолчанию multi-user.target или в похожее.
  • systemctl disable my-name.service – обратная операция по отношению к enable: деассоциировать связь между my-name.service и targets, которые он запросил в [Install] секции своей декларации.
  • systemctl isolate my.target — включить все сервисы из my.target и выключить все остальные включенные сервисы.
  • systemctl status my-name.(service|target) — узнать статус (запущен/остановлен) у сервиса или target.


Надеюсь, эта статья кому-то поможет при осваивании systemd. Я попытался сделать ее компактной, и если упустил из внимания какие-то дополнительные вопросы, спрашивайте в комментариях!

Запуск worker'ов сервиса с помощью systemd 


После выхода Ubuntu 16.04 (новый LTS релиз), systemd стал реальностью всех основных дистрибутивов Linux, использующихся на серверах. Это означает, что можно закладываться на расширенные возможности systemd, не рискуя оставить часть пользователей приложения «за бортом».

Этот пост о том, как реализовать многоворкерное приложение средствами systemd.

Abstract: Использование шаблонов сервисов и target'ов для запуска нескольких инстансов сервиса (реализация «воркеров»). Зависимость PartOf. Немного про [install] секцию у unit'ов.

Вступление


Многие языки программирования с плохой или никакой многопоточностью (Python, Ruby, PHP, довольно часто C/C++) используют концепцию «воркера». Вместо того, чтобы городить сложные отношения между тредами внутри приложения, они запускают несколько однопоточных копий приложения, каждое из которых берёт на себя кусок нагрузки. Благодаря опции SO_REUSEPORT есть даже возможность «вместе» слушать на одном и том же порту, что покрывает большинство задач, в которых возникает потребность в воркерах (собственно, обычные серверные приложения, реализующие API или обслуживающие веб-сайт).

Но такой подход требует наличия «супервизора», который отвечает за запуск копий, следит за их состоянием, обрабатывает ошибки, завершает при всякого рода stop/reload и т.д. При кажущейся тривиальности — это совершенно не тривиальная задача, полная нюансов (например, если один из воркеров попал в TASK_UNINTERRUPTIBLE или получил SIGSTOP, то могут возникнуть проблемы при restart у не очень хорошо написанного родителя).

Есть вариант запуска без супервизора, но в этом случае задача reload/restart перекладывается на администратора. При модели «один процесс на ядро» перезапуск сервиса на 24-ядерном сервере становится кандидатом в автоматизацию, которая в свою очередь требует обработки всех тех же самых SIGSTOP и прочих сложных нюансов.

Одним из вариантов решения проблемы является использование шаблонов сервисов systemd вместе с зависимостью от общего target'а.

Теория


Шаблоны


systemd поддерживает «шаблоны» для запуска сервисов. Эти шаблоны принимают параметр, который потом можно вставить в любое место в аргументах командной строки (man systemd.service). Параметр передаётся через символ '@' в имени сервиса. Часть после '@' (но до точки) называется 'instance name', кодируется %i или %I. Полный список параметров — www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers. Наличие '@' в имени сервиса (перед точкой) указывает на то, что это шаблон.

Попробуем написать простейший template:

/etc/systemd/system/foobar-worker@.service
[Unit]
Description=Foobar number %I
[Service]
Type=simple
ExecStart=/bin/sleep 3600 %I


И запустим несколько таких:

systemctl start foobar-worker@1
systemctl start foobar-worker@2
systemctl start foobar-worker@300


Смотрим:
ps aux|grep sleep
root     13313  0.0  0.0   8516   748 ?        Ss   17:29   0:00 /bin/sleep 3600 1
root     13317  0.0  0.0   8516   804 ?        Ss   17:29   0:00 /bin/sleep 3600 2
root     13321  0.0  0.0   8516   764 ?        Ss   17:29   0:00 /bin/sleep 3600 300


Теперь мы хотим каким-то образом запускать всех их общим образом. Для этого существуют target'ы

Target'ы


Target — это такой юнит systemd, который ничего не делает, но может использоваться как элемент зависимостей (target может зависеть от нескольких сервисов, или сервисы могут зависеть от target'а, который так же зависит от сервисов).

target'ы имеют расширение .target.

Напишем наш простейший target:
vim /etc/systemd/system/foobar.target
[Unit]
Wants=foobar-worker@1.service foobar-worker@2.service
Wants=foobar-worker@300.service

(внимание на .service, оно обязательно!)
Про 'Wants' мы поговорим чуть ниже.

Теперь мы можем запускать все три foobar-worker одновременно:
systemctl start foobar.target
(внимание на target — в случае с .service его можно опускать, в случае с .target — нет).

В списке процессов появилось три sleep'а. К сожалению, если мы сделаем systemctl stop foobar.target, то они не исчезнут, т.е. на «worker'ов» они мало похожи. Нам надо как-то объединить в единое целое target и worker'ов. Для этого мы будем использовать зависимости.

Зависимости


Systemd предоставляет обширнейший набор зависимостей, позволяющий описать что именно мы хотим. Нас из этого списка интересует 'PartOf'. До этого мы использовали wants.
Сравним их поведение:
Wants (который мы использовали) — упомянутый сервис пытается стартовать, если основной юнит стартует. Если упомянутый сервис упал или не может стартовать, это не влияет на основной сервис. Если основной сервис выключается/перезапускается, то упомянутые в зависимости сервисы остаются незатронутыми.
PartOf — Если упомянутый выключается/перезапускается, то основной сервис так же выключется/перезапускается.

Как раз то, что нам надо.

Добавляем зависимость в описание воркера:

[Unit]
Description=Foobar number %I
PartOf=foobar.target
[Service]
Type=simple
ExecStart=/bin/sleep 3600 %I
Всё. Если мы сделаем systemd stop foobar.target, то все наши воркеры остановятся.

Install-зависимости

Ещё одна интереснейшая фича systemd — install-зависимости. В sysv-init была возможность enable/disable сервисов, но там было очень трудно объяснить, как именно надо делать enable. На каких runlevel'ах? С какими зависимостями? В systemd всё просто. Когда мы используем команду 'enable', то сервис «добавляется» (через механизм slice'ов) в зависимость к тому, что мы указали в секции [install]. Для нашего удобства есть зависимость WantedBy, которая по смыслу обратная к Wanted. Есть куча стандартных target'ов, к которым мы можем цепляться. Вот некоторые из них (все — man systemd.special): * multi-user.target (стандартное для «надо запуститься», эквивалент финального runlevel'а для sysv-init). * default.target — алиас на multi-user * graphical.target — момент запуска X'ов Давайте прицепимся к multi-user.target. Новое содержимое foobar.target:
[Unit]
Wants=foobar-worker@1.service foobar-worker@2.service
Wants=foobar-worker@300.service
[install]
WantedBy=multi-user.target
Теперь, если мы его сделаем enable:
# systemctl enable foobar.target
Created symlink /etc/systemd/system/multi-user.target.wants/foobar.target → /etc/systemd/system/foobar.target.
Всё, наш сервис, слепленный из нескольких worker'ов готов запуску/перезапуску как единое целое, плюс его будут запускать при старте нашего компьютера/сервера.

Ссылка