systemd 在启动服务时忽略返回代码

systemd 在启动服务时忽略返回代码

我在为一个简单的守护进程编写单元文件时遇到了这个问题。当守护进程在启动时返回“1”时,systemd 会忽略它,看起来守护进程已成功启动,但实际上它已停止运行。

例如,我有非常简单的 shell 脚本:

#!/bin/bash
exit 1

因此单元文件如下所示:

[Unit]
Description=test service
After=syslog.target

[Service]
User=testuser
Group=testuser
ExecStart=/usr/local/bin/return1

[Install]
WantedBy=multi-user.target

尝试启动,似乎没问题:

# service testservice start
# echo $?
0

但实际上它已经死了:

# service testservice status
● testservice.service - test service
   Loaded: loaded (/etc/systemd/system/testservice.service; enabled)
   Active: failed (Result: exit-code) since Fri 2016-01-22 14:51:45 MSK; 1min 13s ago
  Process: 16416 ExecStart=/usr/local/bin/return1 (code=exited, status=1/FAILURE)
 Main PID: 16416 (code=exited, status=1/FAILURE)

Jan 22 14:51:45 servername systemd[1]: Started test service.
Jan 22 14:51:45 servername systemd[1]: testservice.service: main process exited, code=exited, status=1/FAILURE
Jan 22 14:51:45 servername systemd[1]: Unit testservice.service entered failed state.

看起来 systemd 认为守护进程已成功启动,但后来崩溃了。

我尝试通过将服务类型更改为“forking”和其他类型来解决此问题 - 这在非零代码的情况下工作正常,但服务实际上很“简单”,因此在成功启动的情况下它只会停留并保持终端忙碌。

我该如何管理此类服务?或者可能需要修复守护程序代码中的某些内容?

操作系统 Debian 8 x64,systemd 215

答案1

systemd检测进程是否已成功启动,您必须使用Type=forking,然后在辅助脚本中 fork 您的进程,并检查该脚本是否已成功启动。使用 forksystemd将等待ExecStart命令完成,并检查其退出状态。

您应该像这样更改您的单元文件:

[Unit]
Description=test service
After=syslog.target

[Service]
Type=forking
User=testuser
Group=testuser
ExecStart=/usr/local/bin/fork_service

[Install]
WantedBy=multi-user.target

你应该/usr/local/bin/fork_service看到如下内容:

#!/bin/bash

# Run your process in background
/path/to/your_service &

# Check if the services started successfully 
if ! kill -0 $! 2>/dev/null; then
    # Return 1 so that systemd knows the service failed to start
    exit 1
fi

我在这里只是检查后台进程 PID 是否仍然处于活动状态,但您可以进行任何您想要的检查。唯一重要的是,如果进程成功启动,则此脚本以 0 退出,如果失败,则以正非零值退出。

另外,您不必使用 Bash 来分叉进程,您可以使用任何您想要的语言。

答案2

因此,您希望 systemd 等到进程初始化后再返回。这是一个合理的功能。但是,由于进程初始化可能需要任意长的时间,因此,如果没有正在启动的进程的帮助,systemd 就没有合理的方法知道要等待多长时间。

传统的方法是使用分叉守护进程。守护进程在一个进程中执行初始化,然后在确定可以成功初始化后分叉实际进程。分叉发生并且原始进程成功退出是向 systemd 发出的信号,表明守护进程已初始化。来自 systemd 文档

如果设置为 forking,则预期使用 ExecStart= 配置的进程将在启动过程中调用 fork()。预期在启动完成且所有通信通道都设置好后,父进程将退出。子进程继续作为主服务进程运行,服务管理器将在父进程退出时认为该单元已启动。这是传统 UNIX 服务的行为。如果使用此设置,建议同时使用 PIDFile= 选项,以便 systemd 可以可靠地识别服务的主进程。一旦父进程退出,systemd 将继续启动后续单元。

Systemd 提供了另一种解决方案,在我看来更为优雅。没有必要分叉,只需让守护进程在初始化时直接通知 systemd;这就是 Type=notify。(请参阅我上面链接的文档)

因此,为了修复您的具体示例,您可以修改服务文件以将其Type=notify改为/usr/local/bin/return1

#!/bin/bash                                                                     
exit 1
systemd-notify READY=1

显然,如果初始化失败,通常会有条件地发生 exit 1。初始化完成后,会发送 READY=1。

当您尝试启动它时,这将在命令行上显示预期的错误消息。要查看 systemd 是否实际等待 READY=1,您可以尝试以下命令/usr/local/bin/return1

#!/bin/bash                                                                     
sleep 3
systemd-notify READY=1
sleep 1000000

相关内容