天天看點

docker 運作鏡像_避免在Docker鏡像下将NodeJS作為PID 1運作

docker 運作鏡像_避免在Docker鏡像下将NodeJS作為PID 1運作

在http://elastic.io,我們使用的思想是“每個Docker容器一個程序”。當然,我們也将這種思想應用于運作內建元件。是以,我們的每個內建元件實際上都是一個Docker容器内部的一個程序,并且每個Docker容器都在Mesosphere和Kubernetes上運作。

最近,盡管我們在解決這些過程的确切方式方面遇到了一些無法解釋的問題。編排人員以某種方式認為內建元件一直在發生故障。

一旦找到問題并解決,我們的KPI就會增加,如下圖所示。

docker 運作鏡像_避免在Docker鏡像下将NodeJS作為PID 1運作
docker 運作鏡像_避免在Docker鏡像下将NodeJS作為PID 1運作

那麼,上述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),這意味着正常退出程序。

PS:本文屬于翻譯,原文