使用Python编写systemd服务
在几乎所有Linux发行版都使用systemd的现在,使用Python开发一个守护进程惊人的简单。
创建一个服务
第一步:编写systemd单元
systemd兼并了服务管理工具与监视器工具,systemd既支持系统服务,也支持用户服务。
首先需要创建一个适配服务的systemd单元文件,这个单元文件可以保存在:
- 系统级:
/etc/systemd/system
目录下。 - 全局用户级:
/etc/systemd/user
目录下。 - 用户级:
~/.config/systemd/user/
目录下。
这个单元文件名应当为:
1 | 服务名.service |
内容骨干为:
1 | [Unit] |
就这么简单,接下来我们讲一些详细内容。
标准输出与标准错误
在过去,用户需要将服务的所有输出保存或者重定向到日志文件中,但是对于systemd来说,这一切都不再需要了,因为systemd的journald组件可以收集并记录systemd服务的输出,换句话说,这变成了systemd的任务。
这一行为也可以通过添加这两个选项修改:
1 | [Unit] |
值得一提的是,在默认情况下,Python的输出是带缓冲的,这也就意味着,输出会被阻塞到填满缓冲区后才通过管道传输给systemd,这是不合适的,因此,我们需要关闭Python的输出缓冲,要么给Python解释器加上-u
选项,要么在服务单元中添加以下内容:
1 | [Service] |
使用用户运行
出于安全因素考虑,对于系统级服务,我们也会希望使用独立用户而不是root运行,对于systemd来说,这很容易实现:
1 | [Service] |
还有一种更方便的方法,也就是使用systemd动态生成的临时用户,这种方法是为了取代过去的nobody
公用用户:
1 | [Service] |
自动拉起
systemd包含着过去的Supervisor的功能,也就是服务的自动拉起,要实现也相当简单,只需要添加这个选项:
1 | [Service] |
简单服务与fork服务
在传统的情况下,我们创建一个服务需要从主进程分叉出守护进程,将守护进程的PID保存在PID文件中,然后杀死主进程(参考:PEP 3143)。
但是如之前所说,对于systemd来说,我们不再需要这么麻烦,systemd本身就有能力进行守护进程的生命周期管理和自动拉起,因此我们只需要实现一个简单的前台服务,然后将服务类型设为:
1 | [Service] |
即可。
但是,systemd仍然是可以兼容传统的守护进程逻辑的,只需要给systemd指出服务类型和PID文件路径即可:
1 | [Service] |
此外,如果用户在服务实现中使用了systemd.daemon
库自行给systemd发送信号的话,那么就不需要让systemd对服务的状态进行推断,此时可以将服务类型设为:
1 | [Service] |
我们在之后会再次提到。
停止与垃圾处理
systemd会对服务的生命周期进行管理,这其中当然包括对服务的停止。
我们知道,systemd使用cgroup对服务的所有进程进行追踪,默认情况下,systemd会对服务cgroup内的所有进程发送SIGTERM信号,如果90秒后仍然存在未停止的进程,那么就再对它们发送SIGKILL信号,不过,这一行为可以进行修改:
1 | [Service] |
这些选项给用户实现守护进程的垃圾处理提供了多种方式,比如,用户可以使用KillMode=process
,然后让主进程捕获信号后,再自行处理垃圾和杀死子进程。
除此之外,我们还可以直接让systemd帮忙进行垃圾处理:
1 | [Service] |
服务启用
“启用”一个服务在systemd意味着将服务设置开机启动,这是通过将服务设置为某些target的依赖(一般为多用户模式)实现的,因此只需要添加依赖关系即可:
1 | [Install] |
值得一提的是,如果在主机上不存在用户的任何会话,那么用户的systemd服务也会随之被销毁,要使得用户的服务拥有和系统级服务一样的生命周期,需要给用户启用Linger(参考systemd文档)。
资源限制
systemd可以轻易地对服务进行资源限制,就像这样:
1 | [Service] |
沙箱
systemd也可以对服务进行沙箱化,限制服务对某些服务或网络的使用,从而有效提高安全性,详情请看systemd文档:
1 | [Service] |
编写服务实现
是时候使用Python实现守护进程的主要内容了,一个简单的前台服务只需要实现一个永久的循环即可:
1 | if __name__ == '__main__': |
既然要编写适配systemd的守护进程,那么自然离不开systemd
模块:
1 | import systemd.daemon as sd |
检测系统类型
使用这一函数检测系统是否使用systemd:
1 | systemd.daemon.booted() -> bool |
如果是,那么返回True
,否则返回False
。
发送通知
我们之前提到过notify
,它的功能就是给systemd发送一些服务状态信号,函数签名如下:
1 | systemd.daemon.notify('状态') |
而状态有以下几种:
READY=1
:告诉systemd服务已经启动完成。RELOADING=1
:告诉systemd服务正在重载。STOPPING=1
:告诉systemd服务正在停止。
举个例子,我们可以在启动前进行一些准备工作:
1 | import systemd.daemon |
或者,我们可以在退出前进行一些处理:
1 | def exit(): |
第三步:测试
可以使用systemd-run
工具进行测试,这是一个临时封装systemd单元并运行的命令,它的常见用法为:
1 | systemd-run --unit=任务名 [--user] [--service-type=oneshot|forking] [-G] [-p "PIDFile=/run/xxx.pid"] [-p "SuccessExitStatus=0 1 2"] -d|--working-directory=工作目录 [-E "Key=value"] [-r] 执行的命令 |
选项含义为:
1 | --unit= |