在http://elastic.io,咱们使用的思想是“每一个Docker容器一个进程”。固然,咱们也将这种思想应用于运行集成组件。所以,咱们的每一个集成组件实际上都是一个Docker容器内部的一个进程,而且每一个Docker容器都在Mesosphere和Kubernetes上运行。html
最近,尽管咱们在解决这些过程的确切方式方面遇到了一些没法解释的问题。编排人员以某种方式认为集成组件一直在发生故障。node
一旦找到问题并解决,咱们的KPI就会增长,以下图所示。git
那么,上述KPI发生这种变化的缘由是什么?若是您对技术细节感兴趣,能够详细阅读下面的内容。github
事实证实,NodeJS没法接收信号并正确处理它们(若是它以PID 1运行)。信号是指SIGTERM,SIGINT等内核信号。docker
若是您将NodeJS做为PID 1运行,如下代码将根本没法工做:shell
process.on('SIGTERM', function onSigterm() { // do the cleaning job, but it wouldn't process.exit(0); });
结果,您将得到一个僵尸进程,该进程将经过SIGKILL信号强制终止,这意味着您的“清理”代码将根本不会被调用。api
在http://elastic.io,咱们使用Mesosphere和Kubernetes做为基础平台。当Mesos \ Kubernetes决定终止该任务时,将发生如下状况。 app
Mesos发送SIGTERM,并等待一段时间以使进程终止。若是这种状况没有发生,它将发送SIGKILL(应该能够强制杀死该任务)并将该任务标记为失败的任务。相同的流程适用于Kubernetes。 ide
若是您有一个NodeJS应用程序来侦听RabbitMQ消息,而且不会关闭SIGTERM上的全部侦听器,它将继续侦听而且不会关闭进程,直到SIGKILL能够完成此工做。 this
因为咱们的平台依赖于从Mesos \ Kubernetes返回的状态,所以咱们对任务的状态作出了错误的假设,这对咱们来讲是未知的,而且代表该平台的行为不正确。咱们从不但愿有意想不到的行为,对吗?
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信号来进行最终的终止。
我在这篇文章中找到了很好的解释。
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 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 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),这意味着正常退出进程。