Node.js 进程平滑离场剖析

更新日期: 2019-03-28 阅读: 3k 标签: node

使用 Node.js 搭建 HTTP Server 已是司空见惯的事。在生产环境中,Node 进程平滑重启直接关系到服务的可靠性,它的重要性不容我们忽视。既然是平滑重启,就涉及到新旧进程的接替过渡:

  • 首先,保证新进程平滑入场
  • 其次,保证旧进程平滑离场

本文主要谈论下,在新旧进程接替过渡期间,如何保证旧进程平滑离场。那怎样的离场才算平滑的呢?


如何定义平滑离场

以进程离场作为时间分割点,我们可以把请求分为两类:增量请求和存量请求。

  • 在进程离场前,停止接收新的(增量)请求
  • 在进程离场前,保证未完成的(存量)请求正常响应

所以,达成以上两个目标,基本上我们就认为进程的离场是平滑的。在谈如何做到进程平滑离场前,我们需要一种机制,这种机制能让我们主动通知进程何时离场,这就涉及到进程间通信(IPC)的知识了,我们先简单了解下。


进程间通信

对 Unix 或类 Unix 系统而言,进程间通信的方式有很多种 —— 信号(Signal)是其中的一种。

信号的种类有很多,如 SIGINT、 SIGTERM 及 SIGKILL 等。这些信号视具体需要用于不同的场景,比如 SIGKILL一般用于强杀进程。

我们可以在命令行执行 kill -l 查看所有的信号,如下所示(其中的数字表示 signal number):

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL
 5) SIGTRAP	 6) SIGABRT	 7) SIGEMT	 8) SIGFPE
 9) SIGKILL	10) SIGBUS	11) SIGSEGV	12) SIGSYS
13) SIGPIPE	14) SIGALRM	15) SIGTERM	16) SIGURG
17) SIGSTOP	18) SIGTSTP	19) SIGCONT	20) SIGCHLD
21) SIGTTIN	22) SIGTTOU	23) SIGIO	24) SIGXCPU
25) SIGXFSZ	26) SIGVTALRM	27) SIGPROF	28) SIGWINCH
29) SIGINFO	30) SIGUSR1	31) SIGUSR2

我们可以使用 kill 命令向进程发送指定信号:

# 发送 SIGTERM 信号(默认,无须指定信号类型)给进程
$ kill <pid>

# 发送 SIGINT 信号给进程,其中 <pid> 为具体的进程 ID
$ kill -INT <pid>

# 发送 SIGKILL 信号给进程
$ kill -KILL <pid>

# 或者
$ kill -9 <pid>

进程可以对接收到的信号作出回应。对 Node 应用而言,信号是被当作事件发送给 Node 进程的,进程接收到 SIGTERM 及 SIGINT 事件有默认回调,官方文档是这么描述的:

'SIGTERM' and 'SIGINT' have default handlers on non-Windows platforms that reset the terminal mode before exiting with code 128 + signal number. If one of these signals has a listener installed, its default behavior will be removed (Node.js will no longer exit).

这句话写的很抽象,它是什么意思呢?我们以一个简单的 Node 应用为例。

新建文件,键入如下代码,将其保存为 server.js:

const http = require('http');

const server = http.createServer((req, res) => {
  setTimeout(() => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('It works');
  }, 5000);
});

server.listen(9420);

这里为了方便测试,对应用接收到的每个 http 请求,等待 5 秒后再进行响应。

执行 node server.js 启动应用。为了给应用发送信号,我们需要获取应用的进程 ID,我们可以使用 lsof 命令查看:

$ lsof -i TCP:9420
COMMAND   PID       USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
node    70826 myunlessor   13u  IPv6 0xd250033eef8912eb      0t0  TCP *:9420 (LISTEN)

事实上,我们也可以在代码里通过 console.log(process.pid) 获取进程 ID。这里只是顺便介绍一种,在知道监听 TCP 端口的情况获取进程的方式。

随后,我们发起一个请求,在收到响应之前(有 5 秒等待时间),我们给应用发送 SIGINT 信号。

$ curl http://localhost:9420 &

$ kill -INT 70826
curl: (52) Empty reply from server
[1]+  Exit 52                 curl http://localhost:9420

可以看到,请求没能正常收到响应。也就是说,默认情况下,Node 应用在接收到 SIGINT 信号时,会马上把进程杀死,无视进程还没处理完成的请求。所幸的是,我们可以手动监听进程的 SIGINT 事件,像这样:

process.on('SIGINT', () => {
  // do something here
});

如果我们在事件回调里什么都不做,就意味着忽略该信号,进程该干嘛干嘛,像什么事情都没发生一样。

那么,如果我手动监听 SIGKILL 会如何呢?对不起,SIGKILL 是不能被监听的,官方文档如是说:

'SIGKILL' cannot have a listener installed, it will unconditionally terminate Node.js on all platforms.

这是合情合理的,要知道 SIGKILL 是用于强杀进程的,你无法干预它的行为。

回到上面的问题,我们可以近似地理解为 Node 应用响应 SIGINT 事件的默认回调是这样子的:

process.on('SIGINT', () => {
  process.exit(128 + 2/* signal number */);
});

我们可以打印 exit code 来验证:

$ node server.js

$ echo $?
130

有了信号,我们就能主动通知进程何时离场了,下面谈一谈进程如何平滑离场。


如何让进程平滑离场

我们在上面示例基础上,也就是在文件 server.js 中,补充如下代码:

process.on('SIGINT', () => {
  server.close(err => {
    process.exit(err ? 1 : 0);
  });
});

这段代码很简单,我们改写应用接收到 SIGINT 事件的默认行为,不再简单粗暴直接杀死进程,而是在 server.close 方法回调中再调用 process.exit 方法,接着继续试验一下。

$ lsof -i TCP:9420
COMMAND   PID       USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
node    75842 myunlessor   13u  IPv6 0xd250033ec7c9362b      0t0  TCP *:9420 (LISTEN)

$ curl http://localhost:9420 &
[1] 75878

$ kill -2 75842

$ It works
[1]+  Done                    curl http://localhost:9420

可以看到,应用在退出前(即进程离场前),成功地响应了存量请求。

我们还可以验证,进程离场前,确实不再接收增量请求:

$ curl http://127.0.0.1:9420
curl: (7) Failed to connect to 127.0.0.1 port 9420: Connection refused

这正是 server.close 所做的事,进程平滑离场就是这么简单,官方文档是这么描述这个 api 的:

Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the server is finally closed when all connections are ended and the server emits a 'close' event. The optional callback will be called once the 'close' event occurs. Unlike that event, it will be called with an Error as its only argument if the server was not open when it was closed.


结束语

进程平滑离场只是 Node 进程平滑重启的一部分。生产环境中,新旧进程的接替涉及进程负载均衡、进程生命周期管理等方方面面的考虑。专业的工具做专业的事,PM2 就是 Node 进程管理很好的选择。


本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/2586

相关推荐

怎么卸载nodejs?

Node.js是一个Javascript运行环境,可以使Javascript这类脚本语言编写出来的代码运行速度获得极大提升,那么安装后该如何卸载呢?下面本篇文章就来给大家介绍一下Windows平台下卸载node.js的方法,希望对大家有所帮助。

happypack提升项目构建速度

运行在 Node.js 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。happypack把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

nodejs 异步转同步

nodej项目在微信环境开发,nodejs的异步特效,会导致请求没有完成就执行下面的代码,出现错误。经过多方查找,可以使用async模块来异步转同步,只有前一个function执行callback,下一个才会执行。

node.js反向代理的实现

在实际工程开发中,会有前后端分离的需求。使用node.js反向代理的目的:实现前后端分离,前端减少路径请求的所需的路由文件;通过http-proxy-middleware中间件、Http Proxy 模块这2种方式实现node.js的反向代理

Ubuntu 上 Node.js 安装和卸载

Ubuntu 安装 Node.Js:执行检查可更新的软件,先用普通的apt工具安装低版本的node,然后再升级最新。更换淘宝的镜像,这个是必须的,用过的node的人都知道。安装更新版本的工具N

nodejs 文本逐行读写功能的实现

利用nodejs实现:逐行读写(从一个文件逐行复制到另外一个文件);逐行读取、处理和写入(读取一行,处理后,写入另一个文件)1.所需要的模块: fs,os,readline。功能的实现:readWriteFileByLine.js,功能的调用:index.js

使用pkg打包Node.js应用的方法步骤

Node.js应用不需要经过编译过程,可以直接把源代码拷贝到部署机上执行,确实比C++、Java这类编译型应用部署方便。然而,Node.js应用执行需要有运行环境,意味着你需要先在部署机器上安装Node.js

query和params在前后端中的区别

最近在学node,试着做一个前后端都有的项目,然后就遇到了query和parmas这俩兄弟,你说他们俩长得也不像吧,可这用法实在是太类似了,专门写篇文章来区分这哥俩,分别会从vue路由和Node接收两个角度讲

用node.js开发一个可交互的命令行应用

在这个教程中,我们会开发一个命令行应用,它可以接收一个 CSV 格式的用户信息文件,教程的内容大纲:“Hello,World”,处理命令行参数,运行时的用户输入,异步网络会话,美化控制台的输出,封装成 shell 命令,JavaScript 之外

Node.js 应用:Koa2 使用 JWT 进行鉴权

在前后端分离的开发中,通过 Restful API 进行数据交互时,如果没有对 API 进行保护,那么别人就可以很容易地获取并调用这些 API 进行操作。那么服务器端要如何进行鉴权呢?

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!