在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),這意味着正常退出進程。