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

相关文章
相关标签/搜索