在http://elastic.io我们使用“每个Docker容器一个进程”的思想。当然,我们也把这个想法应用到运行集成组件上。因此,我们的每一个集成组件实际上都是一个Docker容器内部的进程,每个Docker容器都运行在Mesosphere和Kubernetes之上。尽管最近我们遇到了一些无法解释的问题,这些问题究竟是如何解决的。编排器以某种方式认为集成组件一直在失败。一旦发现并解决了问题,我们的KPI就会增加,如下图所示。那么,上述KPI发生这种变化的原因是什么?如果您对技术细节感兴趣,可以详细阅读以下内容。为什么会这样?事实证明,NodeJS无法接收信号并正确处理它们(如果它以PID1运行)。信号是指内核信号,例如SIGTERM、SIGINT等。如果您将NodeJS作为PID1运行,则以下代码将根本不起作用:process.on('SIGTERM',functiononSigterm(){//执行清洁工作,但它不会process.exit(0);});结果,您将获得一个僵尸进程,该进程将通过SIGKILL信号被强行杀死,这意味着您的“清理”代码根本不会被调用。问题出在哪里?在http://elastic.io我们使用Mesosphere和Kubernetes作为底层平台。下面是Mesos\Kubernetes决定终止该任务时发生的情况。Mesos发送SIGTERM,并等待一段时间让进程终止。如果没有发生,它会发送SIGKILL(这应该强制终止任务)并将任务标记为失败。相同的过程适用于Kubernetes。如果您有一个监听RabbitMQ消息的NodeJS应用程序,并且没有关闭SIGTERM上的所有监听器,它将继续监听并且不会关闭进程,直到SIGKILL可以完成这项工作。由于我们的平台依赖于从Mesos\Kubernetes返回的状态,我们对任务的状态做出了错误的假设,这对我们来说是未知的,并且表明平台行为不正确。我们从不想要意想不到的行为,对吗?关于PID1案例的最佳实践是什么?Node.js未设计为以PID1运行,这会导致在Docker内部运行时出现意外行为。例如,以PID1运行的Node.js进程不会响应SIGINT(CTRL-C)和类似信号。(源)翻译大致意味着Node.js未设计为以PID1运行,因此在Docker中运行时会导致意外行为。例如,使用PID1运行的Node.js进程不会响应SIGINT(CTRL-C)和类似信号。想象一下,您有一个用NodeJS编写的应用程序,它正在Mesos\Kubernetes上作为守护进程执行一些工作,等待信号终止它。您有一个SIGTERM的侦听器,您可以关闭SIGTERM上守护程序使用的所有连接。然后守护进程将用退出代码0通知一切正常。NodeJS应用程序甚至无法理解有人要关闭它,所以它只是继续工作并等待SIGKILL信号最终终止。从UNIX的角度对此有何解释?我在这篇文章中找到了很好的解释。但是有一个特例。假设父进程终止,要么是有意的(因为程序逻辑已确定它应该退出),要么是由用户操作引起的(例如,用户终止了进程)。那么它的孩子会怎样呢?它们不再有父进程,因此它们成为“孤儿”(这是实际的技术术语)。这就是init进程发挥作用的地方。init进程——PID1——有一项特殊的任务。它的任务是“收养”孤立的子进程(同样,这是实际的技术术语)。这意味着init进程成为此类进程的父进程,即使这些进程从未由init进程直接创建。NodeJS不打算用作初始化系统。所以,这意味着我们的任何应用程序都必须运行在一些初始化进程中,这将在初始化进程之后产生我们的应用程序,即初始化进程成为应用程序进程的父进程。解决办法是什么?我们如何解决这个问题?我们如何将内核信号传播到我们的应用程序?Dockerinit您可以通过在运行Docker映像时简单地添加标志init来解决此问题:dockerrun--inityour_image_here这将使用一个微型init系统包装您的进程,该系统将利用传递给它的所有内核信号子进程并使确保收割任何孤立的进程。很好,但是如果我们需要重新映射退出代码怎么办?例如,当Java以SIGTERM信号退出时,它返回退出代码143,而不是0。当使用特殊参数“?”报告退出状态时,shell应报告可用的全部八位退出状态。由于收到信号而终止的命令的退出状态应报告为大于128。(来源)Dockerinit无法处理此类情况。因此我们找到了针对这些情况的理想解决方案——Tini。TiniTini是您能想到的最简单的init。Tini所做的只是生成一个子进程(Tini应该在容器中运行),并等待它退出,同时收割僵尸并执行信号转发。(来源)在最新版本中,我们能够将退出代码143重新映射为0,因此我们可以在Docker下运行Java和NodeJS进程:ENTRYPOINT["/tini","-v","-e","143","--","/runner/init"]结论至此,我们已经解决了应用程序中与处理内核信号相关的所有问题,以便它们能够处理并响应它们。此外,我们可以在子进程响应(128+SIGNAL)时重新映射退出代码。也就是说,如果应用程序获得SIGTERM(代码15),在某些情况下它将是143(128+15),这意味着正常退出进程。
