Python守护进程和systemd服务

83

我有一个简单的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(但之前我尝试了没有它,症状类似)。

我应该在我的脚本中实现一些额外的功能,还是systemd文件不正确?


答案解决了您的问题吗?如果没有,请尝试在创建DaemonContext()时设置daemon_context=True。这可能会起作用。 - user59634
1
@pawelbial 很遗憾,你的Python代码示例不完整(缺少daemon的导入,并且不清楚Node来自哪里),因此很难/不可能重现你的情况。 - Jan Vlcinsky
@pawelbial 这与问题间接相关,但可能会对您有所帮助:http://unix.stackexchange.com/a/226853/33386 - Jonathan Komar
5个回答

124

原因是,它没有完成启动序列,这是因为对于类型为forking的服务,启动过程期望进行fork和退出操作(参见$ man systemd.service - 搜索forking)。

只需使用主进程,不要使其成为守护程序

一个选择是做得更少。对于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捕获并记录在日志中。如果没有它,输出将会有所延迟。

为此,我在unit文件中添加了以下行:StandardOutput=syslogStandardError=syslog。如果您不关心日志中的输出,请忽略这些行(它们可以不出现)。

systemd使得daemon化过时了

虽然你的问题标题明确问到了如何进行daemon化,但我猜想,问题的核心是“如何使我的服务运行”,而使用主进程似乎更简单(你根本不用考虑守护进程),这也可以被视为对你的问题的回答。

我认为,许多人之所以使用守护进程仅仅是因为“大家都这么做”。在systemd中,使用守护进程的原因通常已经过时了。可能还有一些使用守护进程的原因,但这种情况现在已经很少见了。

编辑:修正了python -p为正确的python -u。感谢kmftzg。


5
进行守护进程化的原因是为了支持那些不使用systemd的其他平台。必须为systemd单独创建一条代码路径,这也是systemd限制可移植性的另一种方式。 - Nick Bastin
2
@NickBastin 这是另一种可移植性阻碍进步和简化的方式。 - intelfx
2
@NickBastin,systemd支持分叉应用程序,因此如果您不想要,就不需要为systemd编写单独的代码路径。不阅读文档会抑制实际技能并使评论毫无根据。 - Jan Vlcinsky
4
OP提到了“简单的Python脚本”和使用“systemd”。他并没有要求移植到非systemd平台,你指责systemd只存在于你的反应中。 - Jan Vlcinsky
1
我有一个工作中的Python守护进程,使用python-daemon标准模式(基于此处描述的内容:https://dpbl.wordpress.com/2017/02/12/a-tutorial-on-python-daemon/),并且在有或没有systemd的情况下都能很好地工作。我只需注释掉systemd Type值以将其保留为默认值。systemd的启动/停止按预期工作:信号监听以及手动使用--daemon进程运行它:它检查pid和其他重要信息,因此您可以使用几行代码将Python脚本变成守护进程,并使其与systemd兼容。我使用systemd在启动时运行并在出现意外错误时重新启动。 - DGoiko
显示剩余6条评论

22

可以像Schnouki和Amit描述的那样进行守护进程(daemonize)。但是使用systemd,这并不是必要的。有两种更好的初始化守护进程的方法:socket-activation和通过sd_notify()进行显式通知。

Socket activation适用于想要监听网络端口或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#systemd.daemon.listen_fds

显式通知是指守护进程自己打开套接字和/或进行任何其他初始化,然后通知init准备好并可以提供请求服务。这可以使用“forking protocol”实现,但实际上最好的方法是只是用sd_notify()向systemd发送通知。Python包装器被称为systemd.daemon.notify,只需一行代码即可使用[5]。

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

在这种情况下,unit文件应该设为Type=notify,然后调用:

在建立套接字后,使用systemd.daemon.notify("READY=1")通知systemd服务已准备好。不需要分叉或守护进程。

看起来不错。如何通过pip安装提供systemd.daemon的Python库? - guettli
官方安装说明位于 https://github.com/systemd/python-systemd#installation,显示如何使用 pip 进行安装。如果不能正常工作,请在 https://github.com/systemd/python-systemd/issues 提交问题。 - zbyszek

15

您没有创建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")


2
最后一段关于DaemonContext如何改变程序的工作目录,解决了我的守护进程问题。 - DJG
2
由于少量的代码往往更好,我更喜欢答案“只需使用主进程,不要使其成为守护进程”。 - guettli
我不确定Python3中的API是否已更改,但必须使用import daemon.pidfile而不是import daemon.pidlockfile - reox
当锁被解除时,即使守护进程已完成,它仍然保持锁定状态。 - alper

5

当我尝试将一些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

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

我希望我的服务能够自动重启,因此使用了重启选项。我还发现,对于Type来说,使用idlesimple更合理。

idle的行为与simple非常相似;但是,直到所有活动作业被分派之后,才会延迟执行服务二进制文件。这可用于避免shell服务输出与控制台上的状态输出交错。

有关我使用的选项的详细信息,请单击此处

我还尝试保留旧服务并让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启动的进程,systemd脚本则会接管。但是,如果你运行 service <service-name> stop 命令,它将引用旧的init.d服务。所以我发现最好的方法是删除旧的init.d服务,然后用service命令来引用systemd服务。
希望这有所帮助!

4
此外,在创建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

希望这能更好地解释。

2
我可能错了,但我认为这个标志被称为“detach_process”,而不是“daemon_context”。 - Peter Turner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接