运维排查 | Systemd 之服务停止后状态为 failed
哈喽大家好,我是咸鱼。
我们知道 CentOS 7 之后,Systemd 代替了原来的 SystemV 来管理服务,相比 SystemV ,Systemd 能够很好地解决各个服务间的依赖关系,还能让所有的服务同时启动,而不是串行启动。
通常情况下,yum 安装的软件会由系统的包管理器(如 RPM)安装,并且会配置相应的 systemd 服务,因此由 systemd 来管理。
然而,在一些情况下,特别是当采用源码编译安装软件或者软件本身并没有提供 systemd 管理的解决方案时,就需要手动编写 systemd 服务文件(service 文件)来管理这些软件。
那今天我们就来看看手动编写 systemd 服务文件来管理软件时发现的一些问题。
问题出现
我们的 zookeeper 是通过源码编译来安装,为了方便管理,决定改成通过 systemd 来管理。
下面是 zookeeper 的 service 文件:
# zookeeper
[Unit]
Description=Zookeeper
After=network.target
[Service]
Type=forking
ExecStart=/opt/zookeeper/bin/zkServer.sh start
ExecStop=/opt/zookeeper/bin/zkServer.sh stop
PIDFile=/var/lib/zookeeper/zookeeper_server.pid
[Install]
WantedBy=multi-user.target
可以看到,配置文件分成几个区块,每个区块包含若干条键值对。
[Unit]
Unit
部分的 Description
字段给出当前服务的简单描述接下来的设置是启动顺序:
After
字段:表示zookeeper.service
应该在哪些服务之后启动。Before
字段,表示zookeeper.service
应该在哪些服务之前启动。
注意,After
和Before
字段只涉及启动顺序,不涉及依赖关系。
[Service]
Service
部分定义如何启动当前服务。
ExecStart
:启动进程时执行的命令。ExecStop
:停止服务时执行的命令。Type
:定义启动类型
simple(默认值): ExecStart
字段启动的进程为主进程forking: ExecStart
字段将以fork()
方式启动,此时父进程将会退出,子进程将成为主进程oneshot:类似于 simple
,但只执行一次,Systemd 会等它执行完,才启动其他服务dbus:类似于 simple
,但会等待 D-Bus 信号后启动notify:类似于 simple
,启动结束后会发出通知信号,然后 Systemd 再启动其他服务idle:类似于 simple
,但是要等到其他任务都执行完,才会启动该服务。一种使用场合是为让该服务的输出,不与其他服务的输出相混合
[Install]
Install
部分定义如何安装这个配置文件,即怎样做到开机启动
WantedBy
:表示该服务所在的 Target。
Target
的含义是服务组,表示一组服务。WantedBy=multi-user.target
指的是,kafka 和 zookeeper 所在的 Target 是 multi-user.target
。
这个设置非常重要,因为执行systemctl enable
命令时,zookeeper .service
的一个符号链接,就会放在/etc/systemd/system
目录下面的multi-user.target.wants
子目录之中。
Systemd 有默认的启动 Target。
[root@localhost ~]# systemctl get-default
multi-user.target
上面的结果表示,默认的启动 Target 是 multi-user.target
。在这个组里的所有服务,都将开机启动。这就是为什么 systemctl enable
命令能设置开机启动的原因。
编写好 service 文件之后,我们执行下面的命令来启动 zookeeper:
[root@localhost ~]# systemctl start zookeeper.service
接着看下 zookeeper 的运行状态:
[root@localhost ~]# systemctl status zookeeper.service
● zookeeper.service - Zookeeper
Loaded: loaded (/usr/lib/systemd/system/zookeeper.service; enabled; vendor preset: disabled)
Active: active (running) since 一 2024-04-01 09:10:23 CST; 2s ago
Process: 60955 ExecStop=/opt/zookeeper/bin/zkServer.sh stop (code=exited, status=0/SUCCESS)
Process: 61116 ExecStart=/opt/zookeeper/bin/zkServer.sh start (code=exited, status=0/SUCCESS)
Main PID: 61132 (java)
CGroup: /system.slice/zookeeper.service
└─61132 java -Dzookeeper.log.dir=/opt/zookeeper/bin/../logs -Dzookeeper.log.file=zookeeper--server-localhost.localdomain.log -Dzookeepe...
4月 01 09:10:22 localhost.localdomain systemd[1]: Starting Zookeeper...
4月 01 09:10:22 localhost.localdomain zkServer.sh[61116]: /usr/sbin/java
4月 01 09:10:22 localhost.localdomain zkServer.sh[61116]: ZooKeeper JMX enabled by default
4月 01 09:10:22 localhost.localdomain zkServer.sh[61116]: Using config: /opt/zookeeper/bin/../conf/zoo.cfg
4月 01 09:10:23 localhost.localdomain zkServer.sh[61116]: Starting zookeeper ... STARTED
4月 01 09:10:23 localhost.localdomain systemd[1]: Started Zookeeper.
active (running)
表示运行正常,当我们执行 systemctl stop zookeeper.service
命令停止 zookeeper 的时候,问题出现了:
[root@localhost ~]# systemctl status zookeeper.service
● zookeeper.service - Zookeeper
Loaded: loaded (/usr/lib/systemd/system/zookeeper.service; enabled; vendor preset: disabled)
Active: failed (Result: exit-code) since 一 2024-04-01 09:10:30 CST; 906ms ago
Process: 61183 ExecStop=/opt/zookeeper/bin/zkServer.sh stop (code=exited, status=0/SUCCESS)
Process: 61116 ExecStart=/opt/zookeeper/bin/zkServer.sh start (code=exited, status=0/SUCCESS)
Main PID: 61132 (code=exited, status=143)
4月 01 09:10:23 localhost.localdomain systemd[1]: Started Zookeeper.
4月 01 09:10:29 localhost.localdomain systemd[1]: Stopping Zookeeper...
4月 01 09:10:29 localhost.localdomain zkServer.sh[61183]: /usr/sbin/java
4月 01 09:10:29 localhost.localdomain zkServer.sh[61183]: ZooKeeper JMX enabled by default
4月 01 09:10:29 localhost.localdomain zkServer.sh[61183]: Using config: /opt/zookeeper/bin/../conf/zoo.cfg
4月 01 09:10:29 localhost.localdomain systemd[1]: zookeeper.service: main process exited, code=exited, status=143/n/a
4月 01 09:10:30 localhost.localdomain zkServer.sh[61183]: Stopping zookeeper ... STOPPED
4月 01 09:10:30 localhost.localdomain systemd[1]: Stopped Zookeeper.
4月 01 09:10:30 localhost.localdomain systemd[1]: Unit zookeeper.service entered failed state.
4月 01 09:10:30 localhost.localdomain systemd[1]: zookeeper.service failed.
可以看到,zookeeper 服务在停止后并不是 inactive
,而是 failed
状态,最后两行输出里有 Unit zookeeper.service entered failed state./zookeeper.service failed
字段
问题定位
我们接着看上面的输出,可以看到在设置了 Type=forking
后,服务在启动或关闭时执行对应的脚本会开启一个进程,并且两个进程都成功执行了(返回状态码为 0 )。
Process: 61183 ExecStop=/opt/zookeeper/bin/zkServer.sh stop (code=exited, status=0/SUCCESS)
Process: 61116 ExecStart=/opt/zookeeper/bin/zkServer.sh start (code=exited, status=0/SUCCESS)
Main PID: 61132 (code=exited, status=143)
但是主进程退出时返回的状态码却是 143,而不是状态码 0。
接着看下 zookeeper 进程还在不在:
[root@localhost ~]# jps -l
61287 sun.tools.jps.Jps
[root@localhost ~]# ps -ef | grep zookeeper
root 61300 61250 0 09:49 pts/0 00:00:00 grep --color=auto zookeeper
奇怪,明明 zookeeper 进程已经成功退出了,但是 systemd 却说它退出失败。
此时我注意到尽管在停止服务时,状态码为 0,但也只是表明执行 /opt/zookeeper/bin/zkServer.sh stop
命令本身成功完成,这个状态码并不代表脚本内部的执行逻辑一定是成功的。
我们看下 zkServer.sh
脚本中关于 stop 的逻辑
stop)
echo -n "Stopping zookeeper ... "
if [ ! -f "$ZOOPIDFILE" ]
then
echo "no zookeeper to stop (could not find file $ZOOPIDFILE)"
else
$KILL $(cat "$ZOOPIDFILE")
rm "$ZOOPIDFILE"
sleep 1
echo STOPPED
fi
exit 0
;;
没有发现有什么不妥,接着我们注释掉 ExecStop
字段,采用 systemd 默认的方式来停止服务。
默认情况下,systemd 将向进程发送
SIGTERM
信号(相当于kill
命令发送的终止信号),等待一段时间后,如果服务进程未正常退出,则发送SIGKILL
信号(相当于kill -9
命令发送的强制终止信号)强制终止服务进程。
然后重新启停一下 zookeeper ,看下状态:
[root@localhost ~]# systemctl status zookeeper.service
● zookeeper.service - Zookeeper
Loaded: loaded (/usr/lib/systemd/system/zookeeper.service; enabled; vendor preset: disabled)
Active: failed (Result: exit-code) since 一 2024-04-01 10:03:04 CST; 1s ago
Process: 61453 ExecStart=/opt/zookeeper/bin/zkServer.sh start (code=exited, status=0/SUCCESS)
Main PID: 61469 (code=exited, status=143)
4月 01 10:02:55 localhost.localdomain systemd[1]: Started Zookeeper.
4月 01 10:02:55 localhost.localdomain zkServer.sh[61453]: /usr/sbin/java
4月 01 10:02:55 localhost.localdomain zkServer.sh[61453]: ZooKeeper JMX enabled by default
4月 01 10:02:55 localhost.localdomain zkServer.sh[61453]: Using config: /opt/zookeeper/bin/../conf/zoo.cfg
4月 01 10:02:56 localhost.localdomain zkServer.sh[61453]: Starting zookeeper ... STARTED
4月 01 10:03:04 localhost.localdomain systemd[1]: Stopping Zookeeper...
4月 01 10:03:04 localhost.localdomain systemd[1]: zookeeper.service: main process exited, code=exited, status=143/n/a
4月 01 10:03:04 localhost.localdomain systemd[1]: Stopped Zookeeper.
4月 01 10:03:04 localhost.localdomain systemd[1]: Unit zookeeper.service entered failed state.
4月 01 10:03:04 localhost.localdomain systemd[1]: zookeeper.service failed.
[root@localhost ~]# jps -l
61524 sun.tools.jps.Jps
[root@localhost ~]# ps -ef | grep zookeeper
root 61537 61250 0 10:04 pts/0 00:00:00 grep --color=auto zookeeper
还是一样的问题,zookeeper 已经成功退出但是却显示 failed 状态,状态码是 143。
从上面我们得知:无论是通过 zkserver.sh
还是 systemd 默认方式来关闭服务,本质上都是向 zookeeper 进程发送 SIGTERM
信号(数值为 15 ),虽然 zookeeper 进程成功退出,但是 systemd 将此解释为异常退出,因为预期的退出状态码为 0。
而根据 POSIX 规范:【因接收到信号而终止的命令的退出状态应报告为大于 128】,所以被信号中断的进程退出时会返回 128 加上信号数值作为退出状态码。
也就是说,当 zookeeper 进程收到 SIGTERM
信号时,会返回 128 + 15 也就是 143 作为退出状态码,这也就是为什么进程在成功退出后 systemctl 显示为 failed 状态。
解决问题
既然知道了进程在退出时的状态码是 143 但是 systemd 不会解释为成功,因为预期的退出状态码为 0,那么我们只需要让 systemd 把状态码 143 也解释为成功就行。
所以在 zookeeper 的 service 文件中添加下面的配置:
[Service]
...
SuccessExitStatus=143
...
表示当服务进程以状态码 143 正常退出时,systemd 将其视为成功退出而不是异常退出。