使用 systemd 等待直到服务套接字可用,然后启动依赖的服务

Man*_*gor 5 java sockets systemd

目前,我在 systemd 中启动 Java 服务的速度很慢,大约需要 60 秒才能打开其 HTTP 端口并为其他客户端提供服务。

另一个客户端服务期望此服务可用(是此服务的客户端),否则在一定重试后死亡。它也是从 systemd 开始的。这也是一种服务。但是像数据库一样使用前者。

我可以将 systemd 配置为等到第一个服务使他的套接字可用吗?(就像如果套接字实际上正在侦听,那么第二个客户端服务应该启动)。

Luc*_*ter 7

您在这里有多种选择。

\n\n

使用插座单元

\n\n

最优雅的解决方案是让 systemd 为您管理套接字。如果您控制 Java 服务的源代码,请将其更改为 useSystem.inheritedChannel()而不是分配自己的套接字,然后使用如下所示的 systemd 单元:

\n\n
# example.socket\n[Socket]\nListenStream=%t/example\n\n[Install]\nWantedBy=sockets.target\n
Run Code Online (Sandbox Code Playgroud)\n\n

 

\n\n
# example.service\n[Service]\nExecStart=/usr/bin/java ...\nStandardInput=socket\nStandardOutput=socket\nStandardError=journal\n
Run Code Online (Sandbox Code Playgroud)\n\n

systemd 将立即创建套接字(%t是运行时目录,因此在系统单元中,套接字将是/run/example),并在第一次尝试连接时立即启动服务。(如果您希望无条件启动该服务,请在Install其中添加一个部分,并使用WantedBy=multi-user.target。)当您的客户端程序连接到套接字时,它将被内核排队并阻塞,直到服务器准备好接受套接字上的连接。插座。这样做的另一个好处是,您可以重新启动服务,而无需在套接字上出现任何停机 \xe2\x80\x93 连接尝试将排队,直到重新启动的服务准备好再次接受连接。

\n\n

使服务向 systemd 发出就绪信号

\n\n

或者,您可以设置该服务,以便在准备就绪时向 systemd 发出信号,并在其之后为客户端排序。(请注意,这需要After=example.service,而不仅仅是Requires=example.service!依赖关系和排序是正交的 \xe2\x80\x93 After=,如果没有 ,两者将并行启动。)有两种主要服务类型可以使这成为可能:

\n\n
    \n
  • Type=forking:一旦主程序退出,systemd就会认为服务已准备好。由于您可以在 Java 中\xe2\x80\x99t fork,我认为您必须编写一个小的 shell 脚本,该脚本在后台启动服务器,然后等待套接字可用 ( while ! test -S /run/example; do sleep 1s; done)。一旦脚本退出,服务就被认为准备就绪。

  • \n
  • Type=notify:systemd 将等待来自服务的消息,然后才认为它已准备好。理想情况下,消息应该从服务 PID 本身发送:检查是否可以sd_notify通过 JNI/JNA/其他方式(具体来说,sd_notify(0, "READY=1"))从 libsystemd 调用该函数。如果那个\xe2\x80\x99s不可以,你可以使用systemd-notify命令行工具(--ready选项),但是你需要NotifyAccess=all在服务单元中设置(默认情况下,只有主进程可以发送通知),即使这样可能不起作用(systemd 需要在systemd-notify退出之前处理该消息,否则它将无法验证该消息来自哪个 cgroup)。

  • \n
\n


Ale*_*lke 5

初始化过程需要分叉

如果守护进程分叉,systemd 等待守护进程初始化自己。在您的情况下,这几乎是您必须执行此操作的唯一方法。

提供 HTTP 服务的守护进程必须在主线程中完成它的所有初始化,一旦初始化完成并且套接字正在侦听连接,它将fork(). 然后主进程退出。那时 systemd 知道您的进程已成功(退出 0)或未(退出 1)初始化。

这样的服务接收分叉的Type=...值如下:

[Service]
Type=forking
...
Run Code Online (Sandbox Code Playgroud)

“需要”将确保进程等待

其他服务必须等待,因此它们必须要求启动第一个服务。假设你的第一个服务叫做 A,你会有一个像这样的Requires

[Unit]
...
Requires=A
...
Run Code Online (Sandbox Code Playgroud)

有耐心的计划

当然,总有另一种方式让其他服务知道要有耐心。这意味着尝试连接到 HTTP 端口,如果它失败,请休眠一段时间(在您的情况下,1 或 2 秒就可以了)然后再试一次,直到它工作。

我开发了这两种方法,它们都非常有效。

注意:此方法的一个强大方面是,如果服务 A 重新启动,您将获得一个新套接字。当它检测到旧套接字出现故障时,该服务器可以自动重新连接到新套接字。这意味着您在重新启动服务 A 时不必重新启动其他服务。我喜欢这种方法,但是要确保它全部正确实现还需要做更多的工作。

使用 systemd 自动重启功能?

另一种方法可能是使用restart on failure。因此,如果孩子尝试连接到该 HTTP 服务并失败,它应该会失败,对吧?systemd 可以一次又一次地自动重启你的进程,直到它成功。这很糟糕,但如果您无法控制这些守护程序的代码,这可能是最简单的方法。

[Service]
...
Restart=on-failure
RestartSec=10
#SuccessExitStatus=3 7   # if success is not always just 0
...
Run Code Online (Sandbox Code Playgroud)

此示例在尝试重新启动之前在失败后等待 10 秒。

Hack(不得已,不推荐)

您可以尝试 hack,虽然我从来不推荐这样的事情,因为可能会发生破坏这样的事情......在服务中,更改文件以便他们有一个 sleep 60 然后启动主进程。为此,只需编写如下脚本:

#!/bin/sh
sleep 60
"$@"
Run Code Online (Sandbox Code Playgroud)

然后在 .service 文件中,调用该脚本,如下所示:

ExecStart=/path/to/script /path/to/service args to service
Run Code Online (Sandbox Code Playgroud)

这将运行脚本而不是直接运行您的代码。该脚本将首先休眠 60 秒,然后尝试运行您的服务。所以如果由于某种原因这次 HTTP 服务需要 90 秒......它仍然会失败。

尽管如此,了解这一点还是很有用的,因为该脚本可以执行各种操作,例如nc在实际启动服务进程之前使用该工具来探测端口。您甚至可以编写自己的探测工具。

#!/bin/sh
while true
do
  sleep 1
  if probe
  then
    break
  fi
done
"$@"
Run Code Online (Sandbox Code Playgroud)

但是,请注意,这样的循环会一直阻塞,直到probe返回退出代码 0。