1

http://elastic.io,我们使用的思想是“每个Docker容器一个进程”。当然,我们也将这种思想应用于运行集成组件。因此,我们的每个集成组件实际上都是一个Docker容器内部的一个进程,并且每个Docker容器都在Mesosphere和Kubernetes上运行。

最近,尽管我们在解决这些过程的确切方式方面遇到了一些无法解释的问题。编排人员以某种方式认为集成组件一直在发生故障。

一旦找到问题并解决,我们的KPI就会增加,如下图所示。

那么,上述KPI发生这种变化的原因是什么?如果您对技术细节感兴趣,可以详细阅读下面的内容。

为什么会这样呢?

事实证明,NodeJS无法接收信号并正确处理它们(如果它以PID 1运行)。信号是指SIGTERM,SIGINT等内核信号。

如果您将NodeJS作为PID 1运行,以下代码将根本无法工作:

process.on('SIGTERM', function onSigterm() {
  // do the cleaning job, but it wouldn't
  process.exit(0);
});

结果,您将获得一个僵尸进程,该进程将通过SIGKILL信号强制终止,这意味着您的“清理”代码将根本不会被调用。

哪里出现了问题?

http://elastic.io,我们使用Mesosphere和Kubernetes作为基础平台。当Mesos \ Kubernetes决定终止该任务时,将发生以下情况。

Mesos发送SIGTERM,并等待一段时间以使进程终止。如果这种情况没有发生,它将发送SIGKILL(应该可以强制杀死该任务)并将该任务标记为失败的任务。相同的流程适用于Kubernetes。

如果您有一个NodeJS应用程序来侦听RabbitMQ消息,并且不会关闭SIGTERM上的所有侦听器,它将继续侦听并且不会关闭进程,直到SIGKILL可以完成此工作。

由于我们的平台依赖于从Mesos \ Kubernetes返回的状态,因此我们对任务的状态做出了错误的假设,这对我们来说是未知的,并且表明该平台的行为不正确。我们从不希望有意想不到的行为,对吗?

关于PID 1案例的最佳做法是什么?

Node.js was not designed to run as PID 1, which leads to an unexpected behaviour when running inside of Docker. For example, a Node.js process running as PID 1 will not respond to SIGINT (CTRL-C) and similar signals. (source)

翻译大致意思是Node.js并非旨在作为PID 1运行,因此在Docker内部运行时会导致意外行为。例如,以PID 1运行的Node.js进程将不会响应SIGINT(CTRL-C)和类似信号。

想象一下,您有一个用NodeJS编写的应用程序,它作为Mesos \ Kubernetes上的守护程序正在做一些工作,等待信号杀死它。

您具有SIGTERM的侦听器,并且可以关闭守护程序在SIGTERM上使用的所有连接。然后,守护程序将使用退出代码0通知一切正常。

NodeJS应用程序甚至无法理解有人要关闭它,因此它只能继续工作,等待SIGKILL信号来进行最终的终止。

从UNIX角度来看,这有什么解释?

我在这篇文章中找到了很好的解释。

But there is a special case. Suppose the parent process terminates, either intentionally (because the program logic has determined that it should exit), or caused by a user action (e.g. the user killed the process). What happens then to its children? They no longer have a parent process, so they become “orphaned” (this is the actual technical term).

And this is where the init process kicks in. The init process — PID 1 — has a special task. Its task is to “adopt” orphaned child processes (again, this is the actual technical term). This means that the init process becomes the parent of such processes, even though those processes were never created directly by the init process.

NodeJS并非旨在作为初始化系统。因此,这意味着我们的任何应用程序都必须在某个初始化过程中运行,这将在初始化进程的之后生成我们的应用程序,也就是初始化进程成为该应用进程的父级。

解决办法是什么?我们如何解决该问题?我们如何将内核信号传播到我们的应用程序?

Docker init

您可以通过在运行Docker镜像时简单地添加标志init来解决此问题:

docker run --init your_image_here

它将用一个很小的init系统包装您的进程,该系统将利用所有内核信号传递给它的子进程,并确保收获了所有孤立的进程。

没关系,但是如果我们需要重新映射退出代码怎么办?例如,当Java通过SIGTERM信号退出时,它将返回退出代码143,而不是0。

When reporting the exit status with the special parameter ‘?’, the shell shall report the full eight bits of exit status available. The exit status of a command that terminated because it received a signal shall be reported as greater than 128. (source)

Docker init无法处理此类情况。因此,我们找到了针对这些情况的理想解决方案-Tini。

Tini

Tini is the simplest init you could think of. All Tini does is spawn a single child (Tini is meant to be run in a container), and wait for it to exit all the while reaping zombies and performing signal forwarding. (source)

在最新版本中,我们能够将退出代码143重新映射为0,因此我们可以使用以下命令在Docker下运行Java和NodeJS进程:

ENTRYPOINT ["/tini", "-v", "-e", "143", "--", "/runner/init"]

结论

这样,我们解决了与在应用程序中处理内核信号有关的所有问题,从而使它们能够处理它们并做出响应。

另外,当子进程以(128 + SIGNAL)响应时,我们可以重新映射退出代码。也就是说,在应用程序获得SIGTERM(代码15)的情况下,在某些情况下它将是143(128 + 15),这意味着正常退出进程。


iyacontrol
1.4k 声望2.7k 粉丝

专注kubernetes,devops,aiops,service mesh。