Debian.pro/

Про Debian


Пишем свой unit для SystemD

В некоторых следующих статьях помимо прочего придется запускать некоторые сервисы без «изкоробочного» init-скрипта или юнита. В 2018 году мне уже пришлось смириться с победой systemd и показывать в тех статьях init-скрипты я уже не буду. Но и рассказывать в каждой статье всю последовательность действий мне будет лень. Поэтому здесь я расскажу, что делать с unit-ом помимо создания, собственно, текстового файла.
Правда, сначала я немного погружусь в воспоминания и включу ворчалку. Мне очень нравился upstart после SysV. Писать простенький конфиг в 7 строк вместо развесистых init-скриптов — лафа. Хотя иногда и требуется бОльшая гибкость, чем в upstart, или unit-ах, но это редкость. Systemd был бы очень клёвой штукой, хорошим продолжением после upstart, если очкарик не тащил бы в него всё подряд. Основные проблемы systemd связаны не с тем, как он запускает сервисы, а с тем, что внутри systemd вконопачены хреновенький dns, ntp и куча чего-то там ещё. На этом холивар предлагаю закончить — писать unit-ы проще, чем init.d (хоть они и не настолько гибкие).

Какой unit мы напишем?

  • Мы запустим программу /usr/bin/holycrap
  • Она будет выполняться в бесконечном цикле — умерла, запустилась заново (в том и смысл unit-ов основной), пока не скажешь stop. Ну то есть проще говоря, у нас из коробки есть простенький watchdog
  • программу запустим от отдельного пользователя и группы
  • передадим переменную окружения прямо из unit-а

Очень важно понимать, что наша программа должна работать именно в foreground (проще говоря — не отдавать консоль обратно сразу после запуска), дабы не мучаться с ExecStartPre, ExecReload и прочими. Если же программа будет сразу уходить в background, то unit из данного примера будет бесконечно перезапускать её.

Поехали. Создаём файл /etc/systemd/system/holycrap.service

# открываем секцию Unit
[Unit]
# Описание Unit-а (видно в service status). Ну вдруг забудем, что это)
Description=Some usefull text

# Наш сервис будет запускаться после того, как поднимется сеть:
After=network.target

# открываем секцию [Service] - здесь будет описано всё про нашу программу.
[Service]
# указываем, что наш сервис - обычная программа, работающая в foreground
Type=simple
# указываем путь до исполняемого файла:
ExecStart=/usr/bin/holycrap
# указываем WorkingDirectory. По сути, эта настройка эмулирует команду "cd /home/holycrap" перед запуском самого бинаря (нужно для некоторого софта, например nodejs).
WorkingDirectory=/home/holycrap
# указываем параметры для watchdog:
# указываем, что бинарь нужно запустить снова, если он умрет
Restart=on-failure
# указываем, что перед повторным запуском нужно подождать 10 секунд.
RestartSec=10s
# указываем пользователя и группу, от имени которых будет запускаться процесс:
User=holycrap
Group=holycrap
# задаём Umask для процесса и его потомков.
# полезно, если хочется, чтобы все файлы данным процессом создавались с chmod 777 (тогда укажите umask 000), или chmod 700 (тогда umask 077). Не забудьте, что umask - инвертированный chmod.
UMask=077
# ну и передадим переменную окружения PORT=9000, местами бывает удобно:
Environment=PORT=9000

# указываем, что наш сервис нужно запускать в multi-user runlevel (оно тут строго говоря для того, чтобы не запускалось в single mode - так-то нам куда важнее network.target, указанный выше
[Install]
WantedBy=multi-user.target

То же самое, но «удобное для копипасты». Только учтите, что здесь почти нет обязательных параметров (в самых простых случаях я оставляю только Description=, After=, ExecStart= и WantedBy=

[Unit]
Description=Some usefull text
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/holycrap
WorkingDirectory=/home/holycrap
Restart=on-failure
RestartSec=10s
User=holycrap
Group=holycrap
UMask=077
Environment=PORT=9000

[Install]
WantedBy=multi-user.target

В названии файла /etc/systemd/system/holycrap.service важны 2 вещи. Во-первых, файлы без расширения .service будут игнорироваться. Во-вторых, holycrap в данном случае будет названием нашего сервиса (именно его мы будем передавать командам service или systemctl).
Файл мы создали, теперь нужно «применить» его. Учтите, что эту команду нужно выполнять после любого изменения .service-файла, иначе systemd будет использовать старый unit (ну вообще он чё-то там может поругаться, что unit изменился и вообще ничего не делать).

root@server:~# systemctl daemon-reload

Теперь мы можем управлять нашим сервисом (ну и он уже в «автозагрузке», если что):

root@server:~# service holycrap start|stop|restart

Неплохо было бы посмотреть, что наш бинарь понаписал в STDOUT/STDERR (ведь с первого раза по традиции ничего не заработает). Вообще логи с моей точки зрения — больное место systemd. Не потому что плохо работают, а потому что всю эту логику придумал больной на голову человек.
В systemd 236 появились, конечно, параметры StandardOutput=file:/path/to/file и StandardError= (видимо, больной человек голову вылечил и прислушался к пользователям), но проблема тут ещё и в том, что ни в debian 8, ни в debian 9 эти параметры работать не будут (версия старая). Поэтому придется понять логику этого психа.
Посмотреть что-то как-то из STDOUT и STDERR бинаря после запуска сервиса можно так:

root@server:~# service holycrap status

Только вот это будет именно «что-то» и «как-то». Даже длинную строку поскроллить вправо не получится. Зато запомнить легко, ну а там — вдруг повезет.
Предполагается, что каждый пользователь должен уметь в journalctl. Кстати, эта пежня считает себя слишком гордой, чтобы использовать системный pager (less например), поэтому вы будете охуевать, пытаясь посмотреть большой лог, если не сделаете хотя бы | grep (ну или сразу |less).
Посмотрим все логи для нашего unit-а (там, кстати, пишут не только std{err,out}, но и сообщения от самого systemd, если он не смог распарсить .service-файл, например:

root@server:~# journalctl -u holycrap

Если логов слишком дохуя, попробуем посмотреть только с момента загрузки системы (тоже именно для нашего сервиса) — опция -b:

root@server:~# journalctl -b -u holycrap

Проблемы начинаются, когда хочется посмотреть лог для текущего старта (не, ну можно tail -20 и глазами) — вот тут и всплывает инопланетная логика очкарика и его последователей. Так, блять, нельзя О_о. Ну или оно настолько неочевидно в мане и гугле, что за год я так и не смог найти.
Если мы знаем pid процесса, то в целом можно посмотреть лог по пиду. Если он один, мать его… Ах да, смотрите на чудесную инопланетную логику в действии. Чтобы посмотреть логи по пиду, нужно использовать journalctl вот с таким параметром (9999 — сам pid):

root@server:~# journalctl _PID=9999

Я всё же привык к опциям, которые начинаются с дефиса или двух… Может быть с буквы (если это позиционный параметр). Но чтобы с _ ?
Можно посмотреть лог за последние несколько минут (это, кстати, удобно, если не обращать внимания на нашу текущую цель). Если вы помните, когда перезапускали сервис — поможет.

root@server:~# journalctl -b -u holycrap --since="25 min ago"

Можно запустить journalctl в режиме «tail -f» (опять же — для сервиса holycrap) и порестартить сервис в соседней консоли:

root@server:~# journalctl -f -u holycrap

Весело, пздц =) Но сервисы под systemd работают и ладно — я уже как-то привык, если ЁНХ происходит, делать stop сервису и запускать программу руками.

Ну а чтобы разобраться в происходящем, перечислю ваших лучших друзей — man systemd.exec, man systemd.unit, man systemd.service и journalctl —help

А, ну и да, ещё из области инопланетной логики. Не проверяйте exit code у service/systemctl НИКОГДА. Если service выполнился успешно (дескать, вы не попытались запустить сервис с очепяткой), то exit будет нулём, даже если сама программа внутри не запустилась (нет бинаря, или бинарь сделал exit 1).


Комментарии (5):

  1. service не надо, есть systemctl start|stop|restart|status %service_name%

  2. > service не надо, есть systemctl start|stop|restart|status %service_name%
    Бесит, что у systemctl аргументы в неверном порядке =)

  3. Зато можно через пробел перечислить сервисы

  4. Зато нельзя сделать стрелку вверх, ctrl-w, status, ctrl-o, а это нужно намного чаще =)
    Ни тот, ни тот не пишут никакого выхлопа.

  5. Eldarado :

    А Environment=PORT=9000 зачем конкретно нужен?

Написать комментарий