Pull to refresh

SystemD отстой, да здравствует SystemD

Reading time 7 min
Views 50K
Original author: Naftuli Kay
Кажется, что systemd — некое яблоко раздора в Linux-сообществе. Как будто не существует нейтральной точки зрения на systemd. Кардинально противоположные мнения предполагают, что вы должны или любить его, или желать уничтожения. Я хочу предложить некую середину. Для начала, обсудим кошмарные свойства systemd.

Плохое и кошмарное


systemd-escape


Тот факт, что существует systemd-escape, сам по себе явно указывает на нечто ужасно неправильное. Если вы никогда не видели или не использовали эту команду в деле, считайте, что вам повезло.

На практике команда используется примерно так:

/bin/bash -c 'while true; do \
    /usr/bin/etcdctl set my-container \
        "{\"host\": \"1\", \"port\": $(/usr/bin/docker port my-container 5000 | cut -d":" -f2)}" \
    --ttl 60; \
    sleep 45; \
done'

Если честно, это вообще выглядит как плохая идея в целом, но иногда, если вы пишете cloud-init для CoreOS, то это ваш лучший вариант. Escape-символы для новой строки я добавил для лучшей читаемости.

Если нам нужно создать команду ExecStart с таким содержимым, то systemd не сможет распознать кавычки, потому что там он не работает в шелле. Поэтому команда, которая нормально запускается у вас в шелле, не будет работать как юнит systemd. Самым простым решением для systemd было бы реализовать нечто вроде shlex в Python или Shellwords в Ruby, но вместо этого была сделана заплатка словно из потустороннего мира, systemd-escape:

$ man systemd-escape | head
SYSTEMD-ESCAPE(1)                                            systemd-escape                                            SYSTEMD-ESCAPE(1)

NAME
       systemd-escape - Escape strings for usage in system unit names

SYNOPSIS
       systemd-escape [OPTIONS...] [STRING...]

DESCRIPTION
       systemd-escape may be used to escape strings for inclusion in systemd unit names.

Конвертируем скрипт вверху в нечто приемлемое для SystemD:

$ systemd-escape 'while true;do /usr/bin/etcdctl set my-container "{\"host\": \"1\", \"port\": $(/usr/bin/docker port my-container 5000 | cut -d":" -f2)}" --ttl 60;sleep 45;done'
while\x20true\x3bdo\x20-usr-bin-etcdctl\x20set\x20my\x2dcontainer\x20\x22\x7b\x5c\x22host\x5c\x22:\x20\x5c\x221\x5c\x22\x2c\x20\x5c\x22port\x5c\x22:\x20\x24\x28-usr-bin-docker\x20port\x20my\x2dcontainer\x205000\x20\x7c\x20cut\x20\x2dd\x22:\x22\x20\x2df2\x29\x7d\x22\x20\x2d\x2dttl\x2060\x3bsleep\x2045\x3bdone



Согласитесь, что если вам необходимо для работы юнита внедрять цикл while из Bash, то вы уже в плохом положении, но иногда это требуется для шаблонов.

Бинарные логи


Если вы не в курсе, journald хранит свои логи в бинарном формате. Это ломает привычные инструменты, которые мы привыкли использовать для мониторинга системы: tail, cat, less и grep становятся бесполезны. С бинарным форматом возможно и повреждение логов. Если в текстовые логи случайно попадает бинарный контент, большинство редакторов вроде vim и less элегантно его обработают. Если же бинарный контент попадёт в неправильное место бинарных логов, ваши логи потеряны.

Обоснованием хранения логов в бинарном формате были скорость и производительность, их легче индексировать и по ним быстрее искать. Однако это явно был трудный выбор с очевидными последствиями для конечных пользователей на обеих сторонах баррикад. Если требуются быстрые логи/логгирование, то их можно получить, но пользователям придётся выучить новую команду journalctl, и они не смогут использовать знакомые инструменты.

Мне бинарные логи не кажутся чем-то плохим, но это ещё один барьер для принятия systemd. Я рассмотрю логи позже в этой статье и представлю аргументы для своей точки зрения, что journald является хорошей идеей.

Хорошее


Теперь обратим внимание на преимущества, которые нам даёт systemd. Я думаю, именно они стали причиной, по которой все дистрибутивы Linux внедрили systemd.

Здравомыслие


Давайте начнём с того, что сравним init-скрипт SysV для ZooKeeper, который занимает 169 строчек хрупкого кода, как отмечено в комментариях по всему его исходнику.

# for some reason these two options are necessary on jdk6 on Ubuntu
#   accord to the docs they are not necessary, but otw jconsole cannot
#   do a local attach
...

Реализуем его в виде юнита systemd:

[Unit]
Description=ZooKeeper

[Service]
Type=simple
Restart=always
RestartSec=5
EnvironmentFile=/etc/sysconfig/zookeeper
ExecStart=/usr/bin/java -cp ${ZK_CLASSPATH} ${JVM_FLAGS} org.apache.zookeeper.server.quorum.QuorumPeerMain ${ZOO_CFG_FILE}

[Install]
WantedBy=multi-user.target

Я написал это менее чем за десять минут. Разумеется, ему требуется файл окружения, который устанавливает следующие переменные:

ZK_CLASSPATH=/opt/zookeeper:/opt/zookeeper/lib:/etc/zookeeper
JVM_FLAGS=-Xmx=2g
ZOO_CFG_FILE=/etc/zookeeper/zoo.cfg

Но… это всё. Дело сделано.

Если этот процесс просто протоколируется в стандартную выдачу и стандартные ошибки, то логи будут сохранены в журнале, их можно отслеживать, индексировать, искать и экспортировать с помощью syslog-ng или rsyslog. Ниже я рассмотрю, как записываются логи.

Контроль за процессами


В прежние времена мы использовали что-то вроде supervisord для отслеживания, что процессы работают. Это потому что до появления systemd если вы это не напишете, то оно и не произойдёт. Не думайте, что скрипты init после запуска системных служб будут реально мониторить эти процессы, потому что такого не было. Сервисы могли вывалиться с ошибкой segfault и останавливались до тех пор, пока пользователь вручную не запустит их снова.

А теперь systemd:

[Service]
...
Restart=always
RestartSec=1

Это говорит системе, что если произошёл сбой в процессе, то подождать одну секунду и всегда перезапускать его. Если вы указываете stop для сервиса, то он останавливается до тех пор, пока вы снова не запустите его, как и ожидается от этой команды. Вдобавок, systemd записывает в лог, когда и почему произошёл сбой процесса, так что нахождение проблем становится простым и тривиальным.

Планирование процессов


Возвращаясь с тёмные времена init-скриптов SysV, каковы были наши варианты для запуска сервиса после другого сервиса? Или дальше, каковы были варианты для запуска сервиса A после сервиса B, но перед сервисом C? Лучшим вариантом был такой:

while true ; do
  if pgrep serviceB ; then
    start_service
    break
  else
    sleep 1
  fi
done

Для запуска сервиса A перед сервисом C нужно было изменить init-скрипт сервиса C и добавить похожий цикл while для обнаружения и ожидания сервиса A. Не нужно и говорить, что это катастрофа.

А теперь systemd:

[Unit]
Description=Service A
After=serviceB.service
Before=serviceC.service

И это всё. Больше ничего не нужно делать. systemd создаст граф зависимости сервисов и запустит их в правильном порядке, и у вас гарантированно serviceA запустится после serviceB, но перед serviceC. Что ещё лучше, так это drop-in юниты, о которых я скоро расскажу. Если коротко, то несложно включить дополнительный код в исходный юнит без необходимости переписывать код исходного юнита.

Бонус: условия на запуск юнитов


systemd также делает простым задание условий на запуск юнитов:

[Unit]
Description=Service A
ConditionPathExists=/etc/sysconfig/serviceA

Здесь Service A запустится только если присутствует файл /etc/sysconfig/serviceA. Существует много разных условий, и все они могут быть инвертированы.

Бонус: параллелизм


Поскольку systemd знает порядок зависимостей для всех своих юнитов, то загрузка машины Linux с использованием systemd происходит гораздо быстрее, чем на старых init-системах. Это потому что systemd запускает сервисы, не имеющие зависимостей, в параллельном режиме.

Перегрузка юнитами


Как обсуждалось выше, systemd позволяет очень просто подгружать дополнительные конфигурации для заданного юнита, расширяя его. Скажем, нам требуется только запустить rsyslog после запуска cloud-final. В свою очередь, cloud-final является последним этапом работы cloud-init.

Исходный файл для rsyslog.service находится в /usr/lib/systemd/system/rsyslog.service, но мы не будем редактировать этот файл. Мы создадим drop-in юнит в /etc/systemd/system/rsyslog.service.d/after-cloudinit.conf:

[Unit]
After=cloud-final.service

Окончательное название файла не очень важно, поскольку оно заканчивается на .conf. Что бы ни определял этот файл, оно будет включено в оригинальный файл юнита. Этот маленький drop-in проверяет, что rsyslog не запускается, пока cloud-final.service не начался/завершился.

Дополнение: Мне указали в твиттере, что systemd запускает эти файлы в алфавитном порядке. Чтобы сохранить порядок в хаосе, вероятно, хорошей мыслью будет добавлять им числовые префиксы, тогда появится вразумительный порядок, например, %02d-%s.conf.

Перезапись юнитов


Что, если нам требуется полностью удалить из юнита определённые части? Удаление осуществляется просто:

[Service]
ExecStartPre=

Что мы сделали в этом юните для перезаписи, так это удалили все блоки ExecStartPre из исходного юнита. Если мы добавим ещё одну строку ExecStartPre под пустой строкой, то можем указать наш собственный скрипт для предварительного запуска, который полностью отличается от предыдущего.

Запись логов


Логи с systemd невероятно простые и поддерживают все функции и плюшки, какие только захотите. Если процесс просто протоколируется в стандартную выдачу и стандартные ошибки, то лог по умолчанию записывается в журнал. Поиск по этим логам затем тривиален:

$ sudo journalctl -u rsyslog.service

Это запустит браузер типа less для сканирования истории rsyslog.service. Отслеживание лога для юнита тоже осуществляется просто:

$ sudo journalctl -u rsyslog.service -f

Это буквально эквивалент команды tail -f для лога.

Логи автоматически проходят ротацию в журнале и это можно настраивать, так что больше никаких глупостей с logrotate, журнал просто берёт всё на себя.

Подключение rsyslog или syslog-ng делается просто, и это означает, что больше ни одному из ваших приложений не нужно общаться с syslog, их стандартная выдача сразу идёт в журнал и будет импортирована и отправлена в соответствии с конфигурацией syslog.

Идём дальше и учимся


Мы здесь рассмотрели много основных мест. Лично я сохранил в закладках следующие фрагменты документации для systemd, которые помогают мне писать юниты:


Мы даже не говорили о великолепных точках монтирования, таймерах и многих функциях безопасности, которые поддерживает systemd. Я также не упоминал об инструментах из пользовательского пространства systemctl и journalctl, которые описаны здесь:


Определённо, я долгое время был в лагере «systemd отстой», пока не начал изучать, что реально позволяет делать systemd. Теперь я рассматриваю systemd как неотъемлемую часть своей инфраструктуры на системном уровне, и её было бы всё сложнее реализовать на старых дистрибутивах без systemd.
Tags:
Hubs:
+58
Comments 188
Comments Comments 188

Articles