在 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 参数,可以确保子进程不会被清除。
参考资料
unary operator expected解决方法
Linux 系统注册系统服务流程
Systemd使用小结
使用systemctl管理服务