Python демон и системный сервис
У меня есть простой скрипт на Python, который работает как демон. Я пытаюсь создать сценарий systemd, чтобы иметь возможность запустить этот сценарий во время запуска.
Текущий сценарий systemd:
[Unit]
Description=Text
After=syslog.target
[Service]
Type=forking
User=node
Group=node
WorkingDirectory=/home/node/Node/
PIDFile=/var/run/zebra.pid
ExecStart=/home/node/Node/node.py
[Install]
WantedBy=multi-user.target
node.py:
if __name__ == '__main__':
with daemon.DaemonContext():
check = Node()
check.run()
run
содержит while True
петля.
Я пытаюсь запустить этот сервис systemctl start zebra-node.service
, К сожалению, сервис никогда не заканчивал указывать последовательность - я должен нажать Ctrl+C. Скрипт запущен, но статус активируется и через некоторое время меняется на деактивирующий. Сейчас я использую python-daemon (но прежде чем пытался без него, и симптомы были похожи).
Должен ли я реализовать некоторые дополнительные функции в моем скрипте или системный файл неверен?
5 ответов
Причина, по которой он не завершает последовательность запуска, заключается в том, что для типа forking
ожидается, что ваш процесс запуска завершится и завершится (см. $ man systemd.service - поиск разветвления).
Просто используйте только основной процесс, не демонизируйте
Один из вариантов - сделать меньше. При использовании systemd часто нет необходимости создавать демоны, и вы можете напрямую запускать код без демонизации.
#!/usr/bin/python -u
from somewhere import Node
check = Node()
check.run()
Это позволяет использовать более простой тип сервиса под названием simple
, так что ваш файл модуля будет выглядеть так.
[Unit]
Description=Simplified simple zebra service
After=syslog.target
[Service]
Type=simple
User=node
Group=node
WorkingDirectory=/home/node/Node/
ExecStart=/home/node/Node/node.py
StandardOutput=syslog
StandardError=syslog
[Install]
WantedBy=multi-user.target
Обратите внимание, что -u
в python shebang не обязателен, но если вы распечатываете что-то на stdout или stderr, -u
гарантирует, что буферизация на месте отсутствует, и напечатанные строки будут немедленно перехвачены systemd и записаны в журнал. Без этого он появился бы с некоторой задержкой.
Для этого я добавил в файл модуля строки StandardOutput=syslog
а также StandardError=syslog
, Если вам не нужны печатные материалы в вашем журнале, не заботьтесь об этих строках (они не обязательно должны присутствовать).
systemd
делает многих демонов устаревшими
Хотя заголовок вашего вопроса явно спрашивает о демонизации, я думаю, суть вопроса заключается в том, "как заставить мой сервис работать", и хотя использование основного процесса кажется намного проще (вам вообще не нужно заботиться о демонах), оно можно считать ответом на ваш вопрос.
Я думаю, что многие люди используют демонизацию только потому, что "это делают все". При использовании systemd причины демонизации часто бывают устаревшими. Может быть несколько причин использовать демонизацию, но сейчас это будет редкий случай.
РЕДАКТИРОВАТЬ: исправлено python -p
правильно python -u
, спасибо кмфтзг
Можно демонизировать, как описывают Шнуки и Амит. Но с systemd это не обязательно. Есть два способа инициализации демона: активация через сокет и явное уведомление с помощью sd_notify().
Активация сокета работает для демонов, которые хотят прослушивать сетевой порт или сокет UNIX или аналогичный. Systemd откроет сокет, прослушает его, а затем вызовет демона при установлении соединения. Это предпочтительный подход, поскольку он обеспечивает максимальную гибкость для администратора. [1] и [2] дают хорошее введение, [3] описывает C API, а [4] описывает Python API.
[1] http://0pointer.de/blog/projects/socket-activation.html
[2] http://0pointer.de/blog/projects/socket-activation2.html
[3] http://www.freedesktop.org/software/systemd/man/sd_listen_fds.html
[4] http://www.freedesktop.org/software/systemd/python-systemd/daemon.html
Явное уведомление означает, что демон сам открывает сокеты и / или выполняет любую другую инициализацию, а затем уведомляет init, что он готов и может обслуживать запросы. Это может быть реализовано с помощью "протокола разветвления", но на самом деле лучше отправить уведомление systemd с помощью sd_notify(). Оболочка Python называется systemd.daemon.notify и будет одной строкой для использования [5].
[5] http://www.freedesktop.org/software/systemd/python-systemd/daemon.html
В этом случае файл модуля будет иметь Type=notify и вызывать systemd.daemon.notify("READY=1") после того, как он установит сокеты. Нет разветвления или демонизации не требуется.
Вы не создаете файл PID.
systemd ожидает, что ваша программа запишет свой PID в /var/run/zebra.pid
, Поскольку вы этого не делаете, systemd, вероятно, думает, что ваша программа не работает, и, следовательно, деактивирует ее.
Чтобы добавить файл PID, установите lockfile и измените свой код следующим образом:
import daemon
import daemon.pidlockfile
pidfile = daemon.pidlockfile.PIDLockFile("/var/run/zebra.pid")
with daemon.DaemonContext(pidfile=pidfile):
check = Node()
check.run()
(Быстрое примечание: некоторое недавнее обновление lockfile
изменил свой API и сделал его несовместимым с python-daemon. Чтобы исправить это, отредактируйте daemon/pidlockfile.py
, Удалить LinkFileLock
от импорта и добавить from lockfile.linklockfile import LinkLockFile as LinkFileLock
.)
Будьте осторожны в одном: DaemonContext
изменяет рабочий каталог вашей программы на /
, делая WorkingDirectory
вашего служебного файла бесполезен. Если ты хочешь DaemonContext
чтобы перейти в другой каталог, используйте DaemonContext(pidfile=pidfile, working_directory="/path/to/dir")
,
Я сталкивался с этим вопросом, пытаясь преобразовать некоторые службы python init.d в systemd под CentOS 7. Мне кажется, что это прекрасно работает, поместив этот файл в /etc/systemd/system/
:
[Unit]
Description=manages worker instances as a service
After=multi-user.target
[Service]
Type=idle
User=node
ExecStart=/usr/bin/python /path/to/your/module.py
Restart=always
TimeoutStartSec=10
RestartSec=10
[Install]
WantedBy=multi-user.target
Затем я сбросил свой старый файл службы init.d из /etc/init.d
и побежал sudo systemctl daemon-reload
перезагрузить systemd.
Я хотел, чтобы мой сервис автоматически перезагружался, поэтому у меня были варианты перезапуска. Я также нашел использование idle
за Type
имеет больше смысла, чем simple
,
Поведение простоя очень похоже на простое; однако фактическое выполнение двоичного файла службы задерживается до тех пор, пока не будут отправлены все активные задания. Это может использоваться, чтобы избежать чередования вывода служб оболочки с выводом статуса на консоли.
Подробнее о вариантах, которые я использовал здесь.
Я также экспериментировал с сохранением старой службы и перезапуска службы systemd, но столкнулся с некоторыми проблемами.
[Unit]
# Added this to the above
#SourcePath=/etc/init.d/old-service
[Service]
# Replace the ExecStart from above with these
#ExecStart=/etc/init.d/old-service start
#ExecStop=/etc/init.d/old-service stop
Проблемы, с которыми я столкнулся, заключались в том, что вместо службы systemd использовался сценарий службы init.d, если оба были названы одинаково. Если вы убьете процесс, инициированный init.d, сценарий systemd вступит во владение. Но если ты побежал service <service-name> stop
это будет относиться к старой службе init.d. Поэтому я обнаружил, что лучший способ - удалить старую службу init.d, а вместо этого команда service ссылается на службу systemd.
Надеюсь это поможет!
Кроме того, вам, скорее всего, нужно установить daemon_context=True
при создании DaemonContext()
,
Это потому, что если python-daemon
обнаруживает, что если он работает в системе инициализации, он не отсоединяется от родительского процесса. systemd
ожидает, что процесс демона работает с Type=forking
будет делать так. Следовательно, вам это нужно, иначе systemd
будет ждать и, наконец, убить процесс.
Если вам интересно, в python-daemon
Модуль демона, вы увидите этот код:
def is_detach_process_context_required():
""" Determine whether detaching process context is required.
Return ``True`` if the process environment indicates the
process is already detached:
* Process was started by `init`; or
* Process was started by `inetd`.
"""
result = True
if is_process_started_by_init() or is_process_started_by_superserver():
result = False
Надеюсь, это объясняет лучше.