Python 守护进程和 systemd 服务

我有一个作为守护进程的简单 Python 脚本。我正在尝试创建 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

Py:

if __name__ == '__main__':
with daemon.DaemonContext():
check = Node()
check.run()

run包含 while True循环。

我尝试用 systemctl start zebra-node.service运行这个服务。不幸的是,服务没有完成陈述顺序-我必须按 Ctrl + C。 脚本正在运行,但状态正在激活,过了一会儿就会变为不激活。 现在我正在使用 python-daemon (但是在我尝试不使用它之前,症状是相似的)。

我应该在脚本中实现一些额外的特性,还是 Systemd 文件不正确?

86742 次浏览

您没有创建 PID 文件。

Systemd 希望程序将其 PID 写入 /var/run/zebra.pid。由于您不这样做,Systemd 可能认为您的程序正在失败,因此将其关闭。

要添加 PID 文件,请安装 锁定文件并将代码更改为:

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将 chdir 放到另一个目录中,请使用 DaemonContext(pidfile=pidfile, working_directory="/path/to/dir")

另外,在创建 DaemonContext()时,您很可能需要设置 daemon_context=True

这是因为,如果 python-daemon检测到它在 init 系统下运行,它不会与父进程分离。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

希望这能解释得更清楚。

像 Schnuki 和 Amit 描述的那样妖魔化是可能的。但是对于 Systemd 来说,这是没有必要的。有两种更好的方法来初始化守护进程: 套接字激活和使用 sd _ tification ()的显式通知。

套接字激活适用于希望侦听网络端口或 UNIX 套接字或类似内容的守护进程。Systemd 将打开套接字,监听它,然后在连接进入时产生守护进程。这是首选的方法,因为它为管理员提供了最大的灵活性。[1]和[2]给出了一个很好的介绍,[3]描述了 C API,而[4]描述了 Python API。

[1] http://0pointer.de/blog/projects/socket-activation.html
译自: 美国《科学》杂志网站(http://0pointer.de/blog/project/socket-activation2.html) http://0pointer.de/blog/projects/socket-activation2.html
翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳 http://www.freedesktop.org/software/systemd/man/sd_listen_fds.html
译自: 美国《科学》杂志网站(http://www.freedesktop.org/software/systemd/python-systemd/daemon.html # systemd.daemon.listen _ fds) http://www.freedesktop.org/software/systemd/python-systemd/daemon.html#systemd.daemon.listen_fds

显式通知意味着守护进程打开套接字本身并/或执行任何其他初始化,然后通知 init 它已经就绪并可以为请求提供服务。这可以通过“分叉协议”实现,但实际上,使用 sd _ tification ()向 systemd 发送通知更好。 Python 包装器被称为 systemd.daemon.tification,只有一行代码可以使用[5]。

[5] http://www.freedesktop.org/software/systemd/python-systemd/daemon.html#systemd.daemon.notify

在这种情况下,单元文件将具有 Type = tification,并调用 在建立了套接字之后通知(“ READY = 1”)。

之所以没有完成启动序列,是因为对于 forking类型,您的启动过程需要分叉和退出(参见 $man systemd.service-search for fork)。

仅仅使用主进程,不进行守护

一种选择是少做一些。对于 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

注意,python shebang 中的 -u是不必要的,但是如果您将某些内容打印到 stdout 或 stderr,则 -u确保没有任何输出缓冲,并且打印的行将立即被 systemd 捕获并记录在日志中。没有它,它会出现一些延迟。

为此,我在单元文件中添加了代码行 StandardOutput=syslogStandardError=syslog。如果您不关心日记中的打印输出,那么就不要关心这些行(它们不必存在)。

systemd使得守护进程过时

虽然您的问题的标题明确地询问了关于守护进程化的问题,但我猜测,问题的核心是“如何让我的服务运行”,而 使用主流程似乎简单得多(您根本不必关心守护进程) ,它可以被认为是对您的问题的回答。

我认为,许多人使用妖魔化仅仅是因为“每个人都这样做”。随着系统化,妖魔化的原因往往是过时的。使用守护进程化可能有一些原因,但是现在这种情况很少见。

编辑: 固定 python -p到正确的 python -u。感谢 kmftzg

我在试图将一些 python init.d 服务转换为 CentOS 7下的 systemd 时遇到了这个问题。通过将这个文件放在 /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

然后,我从 /etc/init.d中删除了旧的 init.d 服务文件,并运行 sudo systemctl daemon-reload重新加载 systemd。

我希望我的服务自动重新启动,因此重新启动选项。我还发现对 Type使用 idle比使用 simple更有意义。

空闲的行为非常类似于简单; 但是,实际执行 服务二进制的延迟,直到所有活动作业都被分派。 这可以用来避免 shell 服务的输出交错 控制台上的状态输出。

关于我使用的 给你选项的更多细节。

我还尝试了保留旧服务和系统重新启动服务,但是我遇到了一些问题。

[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

我遇到的问题是,如果两者命名相同,则使用 init.d 服务脚本而不是 systemd 服务。如果您关闭了 init.d 启动的进程,Systemd 脚本将接管该进程。但是如果运行 service <service-name> stop,它将引用旧的 init.d 服务。因此,我发现最好的方法是删除旧的 init.d 服务,而 service 命令将引用 Systemd 服务。

希望这个能帮上忙!