注意了!这样用 systemd 可能会有风险

在 Linux 6 / CentOS 6 中,使用 service 来进行服务的起停,但是在 Linux 7 / CentOS 7 中,替换为使用 systemctl 命令来控制。将一些常用应用注册成服务后,可以使用 systemctl 命令来方便的操作启动、停止、重启,但是作者最近发现如果配置不当,严重情况下可能影响正常业务运行,请大家务必关注。

1
2
$ service start loop.service
The service command supports only basic LSB actions (start, stop, restart, try-restart, reload, force-reload, status). For other actions, please try to use systemctl.

问题描述

如果我们有一些负责自动化的应用,例如 puppet / mco、ansible,这里简称为宿主服务。宿主服务注册为系统服务并且随系统开机自启动。宿主服务支持接收服务端指令并拉起一些常驻进程,拉起的进程我们简称为子进程。当宿主服务被 kill 或意外终止时,会引起子进程一起被 kill。

为了复现这个问题,我写了两个脚本。parent_pro.sh 作为宿主脚本,注册为系统 loop.service 并且随系统启动。该脚本的作用是每隔10秒钟检查 config.txt 的配置,如果配置文件中的数字变成 1 则拉起一个无限循环的子进程。具体的代码可以在我的 Github 上看到。

我的 service 定义信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=loop service
After=network.target

[Service]
Type=forking
StandardOutput=/root/namespace/parent_pro.log
StandardError=/root/namespace/parent_pro.log
WorkingDirectory=/root/namespace
User=root
Group=root
ExecStart=/usr/bin/sh parent_pro.sh

[Install]
WantedBy=multi-user.target

下面执行启动服务。

1
2
3
4
5
6
$ systemctl start loop.service
$ ps -ef | grep sh
root 4615 1 0 15:59 ? 00:00:00 /usr/bin/sh parent_pro.sh
root 5153 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh
root 5197 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh
root 5248 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh

从上面的这段输出可以看到,parent_pro.sh 已经启动了三个子进程,我们再看下 parent_pro.sh 的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ systemctl status
● qking-101
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2022-08-11 15:59:54 CST; 29s ago
CGroup: /
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─user.slice
│ └─user-0.slice
│ └─session-1.scope
│ ├─5125 sshd: root@pts/0
│ ├─5129 -bash
│ ├─5224 systemctl status
│ └─5225 less
└─system.slice
├─rsyslog.service
│ └─4618 /usr/sbin/rsyslogd -n
├─postfix.service
│ ├─4944 /usr/libexec/postfix/master -w
│ ├─4948 pickup -l -t unix -u
│ └─4949 qmgr -l -t unix -u
├─loop.service
│ ├─4615 /usr/bin/sh parent_pro.sh
│ ├─5153 sh /root/namespace/child_loop.sh
│ ├─5197 sh /root/namespace/child_loop.sh
│ ├─5198 sleep 10
│ ├─5220 sleep 1
│ └─5222 sleep 1

可以看到我注册的 loop.service 在 system.slice 这个 cgroup 目录下。接下来复现问题场景。

1
2
3
4
5
6
7
8
9
$ kill 4615
$ ps -ef | grep sh
root 1857 2 0 15:59 ? 00:00:00 [kdmflush]
root 1873 2 0 15:59 ? 00:00:00 [kdmflush]
root 3220 2 0 15:59 ? 00:00:00 [kdmflush]
root 4613 1 0 15:59 ? 00:00:00 /usr/sbin/sshd -D
root 5125 4613 0 16:00 ? 00:00:00 sshd: root@pts/0
root 5129 5125 0 16:00 pts/0 00:00:00 -bash
root 5330 5129 0 16:00 pts/0 00:00:00 grep --color=auto sh

杀掉父进程之后,子进程跟着消失了。但是如果手工执行 parent_pro.sh 不会出现这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ nohup sh parent_pro.sh > parent_pro.log  2>&1 &
$ systemctl status
● qking-101
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2022-08-11 15:59:54 CST; 2h 21min ago
CGroup: /
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─user.slice
│ └─user-0.slice
│ ├─session-4.scope
│ │ ├─5797 sshd: root@pts/1
│ │ ├─5801 -bash
│ │ ├─6012 sh parent_pro.sh
│ │ ├─6013 sleep 10
│ │ ├─6014 systemctl status
│ │ └─6015 less
│ └─session-1.scope
│ ├─5125 sshd: root@pts/0
│ └─5129 -bash

修改配置文件让父进程拉起一些子进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
$ echo 0 > config.txt
$ ps -ef | grep sh
root 6012 5801 0 18:21 pts/1 00:00:00 sh parent_pro.sh
root 6018 6012 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6041 6012 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6084 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6147 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6232 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
$ systemctl status
● qking-101
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2022-08-11 15:59:54 CST; 2h 23min ago
CGroup: /
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─user.slice
│ └─user-0.slice
│ ├─session-4.scope
│ │ ├─5797 sshd: root@pts/1
│ │ ├─5801 -bash
│ │ ├─6012 sh parent_pro.sh
│ │ ├─6018 sh /root/namespace/child_loop.sh
│ │ ├─6041 sh /root/namespace/child_loop.sh
│ │ ├─6084 sh /root/namespace/child_loop.sh
│ │ ├─6147 sh /root/namespace/child_loop.sh
│ │ ├─6232 sh /root/namespace/child_loop.sh
│ │ ├─6337 sh /root/namespace/child_loop.sh
│ │ ├─6460 sh /root/namespace/child_loop.sh
│ │ ├─6603 sh /root/namespace/child_loop.sh
│ │ ├─6766 sh /root/namespace/child_loop.sh
│ │ ├─6949 sh /root/namespace/child_loop.sh
│ │ ├─6950 sleep 10
│ │ ├─7069 sleep 1
│ │ ├─7071 sleep 1
│ │ ├─7073 sleep 1
│ │ ├─7075 sleep 1
│ │ ├─7077 sleep 1
│ │ ├─7079 sleep 1
│ │ ├─7081 sleep 1
│ │ ├─7084 sleep 1
│ │ ├─7085 sleep 1
│ │ ├─7087 sleep 1
│ │ ├─7088 systemctl status
│ │ └─7089 less
│ └─session-1.scope
│ ├─5125 sshd: root@pts/0
│ └─5129 -bash
└─system.slice
├─rsyslog.service

这时杀掉父进程不会对子进程产生影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
$ kill 6012
$ ps -ef | grep sh
root 6018 1 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6041 1 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6084 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6147 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6232 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6337 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6460 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6603 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6766 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 6949 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 7154 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 7377 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
root 7620 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh
[1]+ Terminated nohup sh parent_pro.sh > parent_pro.log 2>&1
$ systemctl status
● qking-101
State: running
Jobs: 0 queued
Failed: 0 units
Since: Thu 2022-08-11 15:59:54 CST; 2h 24min ago
CGroup: /
├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
├─user.slice
│ └─user-0.slice
│ ├─session-4.scope
│ │ ├─5797 sshd: root@pts/1
│ │ ├─5801 -bash
│ │ ├─6018 sh /root/namespace/child_loop.sh
│ │ ├─6041 sh /root/namespace/child_loop.sh
│ │ ├─6084 sh /root/namespace/child_loop.sh
│ │ ├─6147 sh /root/namespace/child_loop.sh
│ │ ├─6232 sh /root/namespace/child_loop.sh
│ │ ├─6337 sh /root/namespace/child_loop.sh
│ │ ├─6460 sh /root/namespace/child_loop.sh
│ │ ├─6603 sh /root/namespace/child_loop.sh
│ │ ├─6766 sh /root/namespace/child_loop.sh
│ │ ├─6949 sh /root/namespace/child_loop.sh
│ │ ├─7154 sh /root/namespace/child_loop.sh
│ │ ├─7377 sh /root/namespace/child_loop.sh
│ │ ├─7620 sh /root/namespace/child_loop.sh
│ │ ├─9114 sleep 1
│ │ ├─9116 sleep 1
│ │ ├─9119 sleep 1
│ │ ├─9120 sleep 1
│ │ ├─9122 sleep 1
│ │ ├─9126 sleep 1
│ │ ├─9127 sleep 1
│ │ ├─9128 sleep 1
│ │ ├─9130 sleep 1
│ │ ├─9132 sleep 1
│ │ ├─9134 sleep 1
│ │ ├─9136 sleep 1
│ │ ├─9138 sleep 1
│ │ ├─9139 systemctl status
│ │ └─9140 less
│ └─session-1.scope
│ ├─5125 sshd: root@pts/0
│ └─5129 -bash
└─system.slice

问题分析

仔细查阅了 systemd 相关文档,发现这个问题主要与 type 参数有关。

Type:定义 service 的类型,主要有以下类型:

  • simple:默认类型,启动的程序就是主体程序,这个程序要是退出那么一切皆休。因为 simple 类型不存在主进程退出的情况也就不存在有返回状态的情况,所以它一旦启动就认为是成功的,除非没起来。
  • forking:标准 Unix Daemon 使用的启动方式。启动程序后会调用 fork() 函数,把必要的通信频道都设置好之后父进程退出,留下守护精灵的子进程。如果启动的进程能够自己创建pidfile,最好也指定下PIDFile=XXX。
  • oneshot:一次性服务,这种服务类型就是启动,完成,没进程了。因为这类服务运行完就没进程了,我们经常会需要 RemainAfterExit=yes。后面配置的意思是说,即使没进程了,我们也要 Systemd 认为该服务是存在并成功了的。其他类型千万不要去设置RemainAfterExit=yes,否则systemd会认为服务启动成功了,重启或再去启动都会失败。
  • dbus:这个程序启动时需要获取一块 DBus 空间,所以需要和 BusName= 一起用。只有它成功获得了 DBus 空间,依赖它的程序才会被启动。
  • notify:这个程序在启动完成后会通过 sd_notify 发送一个通知消息。所以还需要配合 NotifyAccess 来让 Systemd 接收消息,后者有三个级别:none,所有消息都忽略掉; main,只接受我们程序的主进程发过去的消息; all,我们程序的所有进程发过去的消息都算。NotifyAccess 要是不写的话默认是 main。
  • idle:这个程序要等它里面调度的全部其它东西都跑完才会跑它自己。比如你 ExecStart 的是个 shell 脚本,里面可能跑了一些别的东西,如果不这样的话,那很可能别的东西的控制台输出里会多一个“启动成功”这样的 Systemd 消息。

最后,如果大家的服务会处理我这种场景,请务必记住这个参数要设置好了,另外还有一个 KillMode 参数也与 cgroup 有关,当我们执行 systemctl stop loop.service 的时候,整个 cgroup 会被删除,通过设置 KillMode 参数,可以确保子进程不会被清除。

参考资料

  1. unary operator expected解决方法
  2. Linux 系统注册系统服务流程
  3. Systemd使用小结
  4. 使用systemctl管理服务

cocowool

A FULL STACK DREAMER!