Docker 容器環境下 Node.js 應用程序的優雅退出

把時間用在思考上是最能節省時間的事情。 —— 卡曾斯node

Docker 容器環境下 Node.js 應用程序的優雅退出,也就是在程序意外退出以後服務進程要接收到 SIGTERM 信號,待當前連接處理完成以後再退出,這樣是比較優雅的,可是在 Docker 容器中實踐時卻發現容器停掉時卻發生了一些異常現象,服務進程並無接收到 SIGTERM 信號,而後隨着容器的銷燬服務進程也被強制 kill 了,顯然當前正在處理的連接也就沒法正常完成了。git

本篇文章主要講解了什麼?github

  • 編寫一個簡單的 Node.js 應用程序實現優雅退出
  • Docker 容器環境下程序優雅退出測試
  • Docker 容器下應用沒法接收退出信號緣由分析
  • Docker 容器環境下構建平滑的 Node.js 應用程序多種實現方案
  • Docker 容器 stop 10s 問題

一個簡單的 Node.js 應用程序

先從一個簡單的例子開始,如下 Node.js 示例,經過 http 監聽 30010 端口,並提供了一個 /delay 接口,實現延遲 5 秒鐘響應請求,這裏我將進程 ID 打印出來是爲了後續測試進程中斷。docker

// app.js
const http = require('http');
const PORT = 30010;
const server = http.createServer((req, res) => {
    if (req.url == '/delay') {
        setTimeout(function() {
            console.log('延遲 5 秒鐘輸出');
            res.end('Hello Docker 延遲 5 秒鐘');
        }, 5000)
    }
})

server.listen(PORT, () => {
    console.log('Running on http://localhost:',PORT, ' PID: ', process.pid);
});
複製代碼
// package.json
{ 
    "name": "hello-docker",
    "main": "app.js",   
    "scripts": { 
      "start": "node app.js"
    }
}
複製代碼

npm 啓動程序npm

npm start

> hello-docker@1.0.0 start /******/hello-docker
> node app.js

Running on http://localhost: 30010  PID:  68971
複製代碼

查看 npm、node 進程信息json

應用程序啓動以後先看下當前進程信息,這裏經過搜索 npm、node 分別將相關進程信息給打印出來,以下所示,細心的你可能會發現 咱們運行 node 程序的進程 ID(68971) 對應的 PPID(68970) 爲 npm 的進程 ID,到這裏也需你就知道了 npm start 的啓動機制,認爲 npm 會將 Node.js 服務作爲本身的子進程啓動,暫時是沒有問題的,繼續往下看。bash

$ ps -falx | head -1; ps -falx | grep 'npm\|node'
  UID   PID  PPID   C STIME   TTY           TIME CMD                     F PRI NI       SZ    RSS WCHAN     S             ADDR
  502 68970 68016   0  4:29下午 ttys003    0:00.35 npm                  4006  31  0  2727120  17304 -      S+                  0
  502 68971 68970   0  4:29下午 ttys003    0:00.12 node app.js          4006  31  0  2682628  14608 -      S+                  0
複製代碼

作一個請求測試網絡

作一個測試,我開始請求接口,控制檯執行 curl http://localhost:30010/delay 請求,同時我又新打開另外一個控制檯當即執行 kill -15 68970 這個時間是在 5 秒中以內,能夠看到個人請求獲得了一個錯誤的響應併發

kill -15:是發送一個 SIGTERM 信號,該信號可由應用程序捕獲, 故使用 SIGTERM 也讓程序有機會在退出以前作好清理工做, 從而優雅地終止。app

# 請求接口
$ curl http://localhost:30010/delay

# kill 殺掉進程
$ kill -15 68970

# 響應報錯
curl: (52) Empty reply from server

# 上面啓動的程序也會報以下錯誤 terminated npm start
> hello-docker@1.0.0 start /******/hello-docker
> node index.js

Running on http://localhost: 30010  PID:  68971
zsh: terminated  npm start
複製代碼

這個結果顯然不是咱們須要的,接下來咱們要在增長一些處理,實現優雅退出

實現 Node.js 程序優雅退出

優雅退出:程序接收到 SIGTERM 信號,執行清理工做,釋放本身正在處理的一些資源以後自行退出,常見的例如,程序接收到一個 HTTP 請求正在處理,若是忽然間中斷了,用戶端也就沒法正常的收到響應了,經過優雅退出咱們先要保證當前正在處理的連接可以正常的被響應。

咱們的程序默認是不會去監聽這項工做的,須要顯示的監聽該信息,在資源釋放完成以後執行 process.exit(0) 退出進程。

改造 app.js

const http = require('http');
const PORT = 30010;
const server = http.createServer((req, res) => {
    if (req.url == '/delay') {
        setTimeout(function() {
            console.log('延遲 5 秒鐘輸出');
            res.end('Hello Docker 延遲 5 秒鐘');
        }, 5000)
    }
})

/** 改造部分 關於進程結束相關信號可自行搜索查看*/
process.on('SIGTERM', close.bind(this, 'SIGTERM'));
process.on('SIGINT', close.bind(this, 'SIGINT'));

function close(signal) {
    console.log(`收到 ${signal} 信號開始處理`);

    server.close(() => {
        console.log(`服務中止 ${signal} 處理完畢`);
        process.exit(0);
    });
}
/** 改造部分 */

server.listen(PORT, () => {
    console.log('Running on http://localhost:',PORT, ' PID: ', process.pid);
});
複製代碼

再次 npm 開啓咱們的服務進行測試

$ npm start
$ ps -falx | head -1; ps -falx | grep 'npm\|node'
  UID   PID  PPID   C STIME   TTY           TIME CMD                     F PRI NI       SZ    RSS WCHAN     S             ADDR
  502 70990 68016   0  6:51下午 ttys003    0:00.48 npm                  4006  31  0  2727604  38136 -      S+                  0
  502 70991 70990   0  6:51下午 ttys003    0:00.13 node app.js          4006  31  0  2682628  23196 -      S+                  0
$ 
複製代碼

請求測試

$ curl http://localhost:30010/delay
$ kill -15 70990 # 中斷進程
複製代碼

此時服務並不會立刻退出,會顯示以下日誌信息,等待連接處理完畢以後進程退出

Running on http://localhost: 30010  PID:  70991
收到 SIGTERM 信號開始處理
延遲 5 秒鐘輸出
服務中止 SIGTERM 處理完畢
複製代碼

Docker 環境下測試

這裏假設你已經瞭解了 Docker 的基本操做和在 Node.js 中的應用,不清楚的你須要先看下這兩篇介紹 一文零基礎教你學會 Docker 入門到實踐Node.js 服務 Docker 容器化應用實踐

啓動容器

$ docker run -d -p 30010:30010 hello-docker
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                      NAMES
c73389c8340f        hello-docker        "npm start"         6 minutes ago       Up 6 minutes        0.0.0.0:30010->30010/tcp   crazy_archimedes
複製代碼

查看日誌

$ docker logs -f c73389c8340f

> hello-docker@1.0.0 start /usr/src/nodejs
> node app.js

Running on http://localhost: 30010  PID:  16
複製代碼

請求測試

$ curl http://localhost:30010/delay
$ docker stop c73389c8340f
複製代碼

在我請求 http://localhost:30010/delay 以後當即執行中止容器操做,並無按照個人預期正常退出,而是報出了 curl: (52) Empty reply from server 錯誤,顯然個人 Node.js 應用沒有接收到退出信息,隨着容器的銷燬被強制退出了,什麼緣由呢?接下來我會分析下產生這個狀況的緣由

$ curl http://localhost:30010/delay
curl: (52) Empty reply from server
複製代碼

Docker 容器下應用沒法接收退出信號緣由分析

這裏我從容器內進程的聲明週期、NPM 啓動機制、信號的傳遞機制進行分析

容器內進程的生命週期

上面舉的 Node.js 例子在非容器環境下是能夠實現優雅退出的,可是在 docker 容器環境卻不行,那咱們先來了解下容器內進程的生命週期是怎麼樣的。

在 Docker 中多個容器(Container)間的進程是相互隔離的,例如,Container1 我有個 init 進程 PID=1,Container2 中一樣也是,所以,容器與其它容器及其主機是隔離的,且擁有本身的獨立進程空間、網絡配置。

Docker 容器啓動的時候,會經過 ENTRYPOINT 或 CMD 指令去建立一個初始化進程 PID=1,這個 PID=1 的進程會根據本身的指令建立本身的子進程,在這個容器內部,進程之間會造成一個層級關係,即進程樹的概念,當容器退出時也會經過信號量來通知 PID=1 的進程,而後這個會通知本身的子進程等等,這個涉及 Unix 進程相關知識,父進程會等待全部子進程結束,並獲取到最終的狀態。最終當這個 PID=1 的進程退出以後,Docker 容器也將銷燬併發送 SIGKILL 信號量通知容器內其它還存在的進程,此時就是強制退出了。

這樣看來彷佛並無發現什麼問題,難道 npm 啓動 Node 程序有問題?

容器內 NPM 的啓動機制

這裏我要分析下在容器環境和非容器環境下 NPM 的啓動有什麼不一樣,另外咱們在啓動 Node.js 應用程序的時候一般也會將啓動命令寫在 package.json 的 scripts 裏面,經過 npm run ... 進行啓動

非容器環境下的 npm 啓動 Node.js

非容器環境下,經過 npm 進程直接啓動了 node 進程,如下示例也能看到 node 的父進程(PPID=70990)

$ npm start
$ ps -falx | head -1; ps -falx | grep 'npm\|node'
  UID   PID  PPID   C STIME   TTY           TIME CMD                     F PRI NI       SZ    RSS WCHAN     S             ADDR
  502 70990 68016   0  6:51下午 ttys003    0:00.48 npm                  4006  31  0  2727604  38136 -      S+                  0
  502 70991 70990   0  6:51下午 ttys003    0:00.13 node app.js          4006  31  0  2682628  23196 -      S+                  0
$ 
複製代碼

容器環境下的 npm 啓動 Node.js

Docker 容器環境經過 Dockerfile 文件指定 CMD ["npm", "start"] 指令啓動 Node.js,如下打印出了進程列表信息,另外我經過 pstree -p 打印出了進程之間的層級關係,這下很清晰了在容器環境下,npm 作爲 INIT 進程啓動以後,並無直接去啓動 node 進程,而是先啓動了 sh 進程,而後 sh 進程啓動了 node 進程,這和上面的在非容器環境下仍是有區分的。

執行 docker stop 命令以後,首先 npm 會收到 SIGTERM 信號量,而後轉發給 sh,此時咱們理解的多是 sh 在轉發給 node 若是真的是這樣也就沒問題了,問題就出在當 SIGTERM 到達 sh 以後,就斷片了,sh 本身退出了,node 進程就只好等待容器銷燬被強制退出。

$ ps flex
PID   USER     TIME   COMMAND
    1 root       0:00 npm
   15 root       0:00 sh -c node app.js
   16 root       0:00 node app.js

$ pstree -p
npm(1)---sh(15)---node(16)
複製代碼

Docker 容器環境下 Node.js 服務優雅退出多種實現方案

在上面瞭解了 Docker 環境沒法,Node.js 沒法正常優雅退出的緣由,如下給出幾種解決方案

Node 進程作爲容器主進程

修改 Dockerfile 文件,直接使用 node app.js 運行而不是經過 npm

CMD [ "node", "app.js" ] 複製代碼

修改以後從新構建鏡像,運行容器,彷佛達到了個人預期,init 進程爲 node 進程

$ docker image build -t hello-docker .
$ docker container run -d -p 30010:30010 hello-docker

# 先進入容器,執行 ps flax、pstree -p
$ ps flax
PID   USER     TIME   COMMAND
    1 root       0:00 node app

$ pstree -p
node(1)
複製代碼

執行請求以後,當即中止容器,響應也是 ok 的,從容器內查看服務的日誌也可看到是收到了進程退出的信號。

$ curl http://localhost:30012/delay
$ docker stop e816ef6290a0
Hello Docker 延遲 5 秒鐘

# 容器的日誌 docker logs -f e816ef6290a0 命令查看
Running on http://localhost: 30010  PID:  1
收到 SIGTERM 信號開始處理
延遲 5 秒鐘輸出
服務中止 SIGTERM 處理完畢
複製代碼

總結 Node 進程作爲容器主進程: 這種方案雖使用簡單,可是缺乏 npm script 這種可使咱們在啓動前提供不少配置選項的功能,使用 npm script 咱們能夠配置一些複雜的啓動命令。

消除中間的 sh 進程

這種方案是在 npm 啓動以後,消除 npm 與 node 之間的 sh 進程,exec node app.js,簡單解釋下 exec 會用新的進程去替換以前的進程,這樣以前的 sh 進程也就消失了。

修改 package.json

// package.json
{ 
    "name": "hello-docker",
    "main": "app.js",   
    "scripts": { 
      "start": "exec node app.js"
    }
}
複製代碼

修改 Dockerfile

仍是以前的 npm script 啓動

CMD ["npm", "start"] 複製代碼

查看容器內進程信息

經過 pstree -p 命令,能夠看到啓動後的進程樹爲 npm(1)---node(15),中間已沒有了 sh 進程

# 進入容器內
$ docker exec -it d5f16c6ffa91 /bin/sh 

$ ps flax
PID   USER     TIME   COMMAND
    1 root       0:00 npm
   15 root       0:00 node app.js

$ pstree -p
npm(1)---node(15)
複製代碼

其它方案

社區中也不乏有其它的解決方案,可參考如下幾個項目

Egg.js 框架

在基於 Egg 框架的項目中進行測試時,並無如上的這些問題,如下是在容器內打印的進程樹,能夠看到 npm 的進程 id 爲 1,以後就直接爲 node 進程,這應該是框架內本身作的處理,感興趣的能夠去研究下實現機制。

$ pstree -p
npm(1)---node(24)---node(39)-+-node(46)
                             |-node(73)
                             `-node(74)
複製代碼

Docker 容器 stop 10s 問題

如下對 app.js 作了改造,將原先等待 5 秒,設置爲了 15 秒,在進行測試下

const server = http.createServer((req, res) => {
    if (req.url == '/delay') {
        setTimeout(function() {
            console.log('延遲 15 秒鐘輸出');
            res.end('Hello Docker 延遲 15 秒鐘');
        }, 15000)
    }
})
複製代碼

當我執行接口請求以後,當即執行了 docker stop f2206f06472e 命令,發現又報了以下錯誤,感受又回到瞭解放前,上面的方案不是均可以嗎?

$ curl http://localhost:30010/delay
curl: (52) Empty reply from server
複製代碼

上面的方案是沒有問題的,暴露出來了另一個問題,在執行 docker stop [containerID] 命令時候,有一個默認 10S 的問題,其有如下一段描述,意思爲容器內的主進程在必定時間內將會收到一個 SIGTERM 信號,這個時間官方默認爲 10 秒,超過這個時間將會收到 SIGKILL 信號,被暴力退出。

docs.docker.com/engine/refe…

The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL
複製代碼

所以,在必要狀況下,能夠在 docker stop 命令後設置一個 -t 選項來調整這個時間

$ docker stop -t=15 d90bab781031
複製代碼

Refenrce

相關文章
相關標籤/搜索