过去我们项目组的应用都是用 supervisord 托管的。最近因为某些因素,无法使用 supervisord,因此考虑改用 systemd。
作为主流 Linux 发行版的默认选项,之前多多少少用过一点 systemd。不过这次需要上生产环境,所以抽空深入研究一番。
为什么要用 supervisord?
- 实现进程的分组管理,比如支持一同启动/停止多个生产者/消费者实例。
- 进程崩溃的时候可以重启
要想改用 systemd,需要看下 systemd 如何应对这两个问题。
(如无指明,在本文中,supervisord 的配置项在 [program:x]
下面,而 systemd 的配置项则位于 [Service]
)
进程控制
无论 supervisord 还是 systemd,都采用 ini 作为配置文件的格式。跟 supervisord 不同的是,systemd 每个程序都要单独开一个 unit 文件。
supervisord 可以同时启动/停止配置文件中所以的进程(或者某个进程组配置中的进程)。systemd 可以用依赖来实现这一点。下面例子中,我们
就创建了一个可以同时管理的进程组:
; group.target
[Unit]
Description=Application
Wants=prog1.service prog2.service
; prog1.service
[Unit]
Description=prog1
PartOf=group.target
[Service]
ExecStart=/usr/bin/prog1
Restart=on-failure
; prog2.service
[Unit]
Description=prog2
PartOf=group.target
[Service]
ExecStart=/usr/bin/prog2
Restart=on-failure
systemctl start group.target
,prog1
和 prog2
也会带起来。systemctl restart group.target
,prog1
和 prog2
也会跟着重启。
相对来说,supervisord 的做法更加直观一些。
如果要更改 supervisord 的配置文件,supervisord 需要运行 supervisorctl reread
才会生效。
而 systemd 则需要 systemctl daemon-reload
。半斤八两吧。
不过 supervisord 有一个好。如果你不知道哪些程序的配置改变了,简单地执行 supervisorctl update
,所有涉及的进程都会被重启。
而 systemd 貌似做不到这一点。
systemd 可以指定 stop 操作时可以选择的命令(ExecStop=
)。另外它还提供了 ExecReload=
,可以自定义调用 systemctl reload xxx
重新读取程序配置文件时的操作。
supervisord 不支持 reload 指定进程。同时对于 stop 操作,它只允许你选择要发送哪种信号…
supervisord 的 stopwaitsecs
可以控制 stop 操作后等待程序退出的耐心(以秒衡量)。待给定的耐心都消耗完毕后,supervisord 才会痛下杀手,发送 SIGKILL。
systemd 对应的配置项是 TimeoutStopSec=
。systemd 会给多一次机会,在下最后通牒之前,会先发送 SIGTERM,过另一个 TimeoutStopSec 之后才发送 SIGKILL。
进程重启
为了避免在 supervisord 和 systemd 两套术语间迷糊,请允许我抛弃所有术语,用自己的话描述。
从上图可以看到,进程在 RUNNING 之前,会有一个 STARTING 的过程。在 STARTING 过程中,进程可能会读取配置文件,进行初始化。
这一过程中的错误处理,跟 RUNNING 状态的应当有所不同。
以上是 supervisord 的想法。
所以 supervisord 提供了单独的 startretries
配置项,用来配置 STARTING 阶段的重启次数。
systemd 对此没有特殊处理。
一个程序,从 RUNNING 到 EXITED,有两种可能:正常退出或异常退出…(废话)
这两种情况,是通过配置的退出码来区分的。对于 supervisord,这个配置项是 exitcodes
。systemd 则通过 SuccessExitStatus
来控制。
有趣的是,exitcodes
的默认值是 0,2
,不知道为何它会认为 2 也是正常的退出码。
如果配置了 autorestart = true
,只要程序退出,supervisord 都会把它启动起来。相对的,如果配置的是 autorestart = unexpected
,则只有
异常退出才会重启。这两个选项,在 systemd 里对应 Restart=always
和 Restart=on-failure
。systemd 还提供了 Restart=on-success
(只有正常
退出才重启)和 Restart=on-abort
(只有收到异常信号才重启)。
对于重启次数,supervisord 没有作限定。因为重启一个程序时,supervisord 会先让它处于 STARTING 状态。这个状态的持续时间,是由配置项
中的 startsecs
决定的,默认 1 秒。如果是不可恢复的错误,程序就不可能成功进入到 RUNNING 状态。当然也许存在这样的情况,程序运行 1 秒
后,就会崩溃。那么它就会陷于不停重启的无间地狱。
systemd 对此一如既往,提供了 N 多选项以供采用。你可以用 RestartSec
控制每次重启的间隔,可以用 StartLimitInterval
和 StartLimitBurst
设定
给定周期内能够重启的次数。比如指定 StartLimitInterval=1s
,StartLimitBurst=3
,就可以实现跟 supervisord 一致的默认重启策略。
比较完最基本的两种功能,让我们继续看看,两者在一些小细节上的对对碰。
控制实例数
supervisord 可以用 numprocs
来控制单个程序对应的实例数。systemd 也可以做到这一点,虽然有点麻烦(某种意义上,更加强大)。
systemd 会把以 @
结尾的 service
文件当作模板,在运行时根据给定的参数展开成多个实例。
具体实现方式见:http://0pointer.de/blog/proje…
日志
supervisord 能够重定向被托管的程序的 stdout 和 stderr 到日志文件中,并提供日志切割服务。systemd 也支持这一点,尽管它的实现有很大的不同。
根据鄙人的经验,基于定期检查的日志切割服务,不是个好的选择。
一旦遇上突发高峰,有可能会出现日志无法及时切割的情况;而调小检查间隔,大部分情况下都在无意义地空转。(说的就是你,logroated)
好在无论是 supervisord,还是 systemd,提供的切割服务都是实时的。每当写入内容会超过上限时,就会自动切割。
systemd 的日志服务是通过 journald 组件实现的。你可以在 /etc/systemd/journald.conf
中配置它。
journald 默认的日志存储形式是 Storage=auto
。这个选项比较奇妙,如果你创建了 /var/log/journal
文件夹,那么它就会把日志写到这个文件夹下。否则不进行持久化。
持久化后的日志是这个样子的:
/var/log/journal/c4010ceea79847afbedecb60a775db96/
├── system.journal
├── user-1000.journal
└── user-65534.journal
第一次看到这样的目录结构,说不定你会大吃一惊。journald 设计者脑洞不是一般的大。从这个结构上,根本看不出应用日志在哪里嘛。
不,完全没有这样的必要,因为所有的程序的日志都会写到一块去。不分彼此,全变成一团浆糊。随便一提,日志默认都是压缩的。
要看日志,你得用 journalctl
。比如看 prog1.service
的日志,需要 journalctl -u prog1.service
。要看特定时期的日志,需要 journalctl --since $timestamp --until $timestamp
。
这么前卫的设计我可接受无能。这种 journalctl
控制一切的方式,导致 systemd 日志无法集成到传统的日志收集工具中。
程序员工具箱中各种 text base
处理工具,对此也大眼瞪小眼,只能对着 journalctl
低三下四,接受对方的小脾气。
journald 提供了三个配置项,RuntimeMaxFileSize=
和 RuntimeMaxFiles=
。顾名思义,就是单个日志文件大小和允许的日志数。
另外,RuntimeMaxUse=
和 RuntimeKeepFree=
可以控制总大小的上限。
supervisord 在这方面做的要好得多。通过 stdout/stderr_logfile_maxbytes
和 stdout/stderr_logfile_backups
,你可以规划每一个程序的日志文件的切割粒度。
不同程序的日志不会挤一起,产生日志少的程序也不会被产生日志多的程序干扰。
systemd vs supervisord
除了以上几点外,还有一些没有具体提到的功能。
比如 supervisord 通过 priority 配置进程启动顺序,以及 systemd 对应的 Before/After 依赖机制。
比如 supervisord 的 events 功能,和与之相对应的 systemd 的 notify 机制。
比如 supervisord 可以管理 fastcgi(真有人这么做吗)。
比如 systemd 提供的基于 cgroup 的资源限制。
由于没有使用经验,对这些功能就不作一一比较了。
是时候结题了。
systemd 和 supervisord 各有长短,不存在哪一方绝对的碾压。
systemd 跟 Linux 紧密结合,所需的依赖少,其提供的保障自然比 supervisord 更可靠。然而在强大的能力背后,也有配置复杂、不易上手等问题。
supervisord 偏于应用层,却因此有独特的用武之地。
举个例子,许多人会往 docker 打包里面封入一份 supervisord,让它来做 PID 1,以此稍微增强下健壮性。
换 systemd 做同样的事,就像用园艺剪刀裁纸,即使能够顺利完成,也难免事倍功半。毕竟这样的方式跟 systemd 的设计是背道而驰的。