Node.js 进程平滑离场剖析

本文由云+社区发表html

做者:草小灰node

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

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

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

如何定义平滑离场

以进程离场做为时间分割点,咱们能够把请求分为两类:增量请求存量请求less

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

因此,达成以上两个目标,基本上咱们就认为进程的离场是平滑的。在谈如何作到进程平滑离场前,咱们须要一种机制,这种机制能让咱们主动通知进程什么时候离场,这就涉及到进程间通讯(IPC)的知识了,咱们先简单了解下。curl

进程间通讯

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

信号的种类有不少,如 SIGINTSIGTERMSIGKILL 等。这些信号视具体须要用于不一样的场景,好比 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 命令向进程发送指定信号:ui

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

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

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

# 或者
$ kill -9 <pid>

进程能够对接收到的信号做出回应。对 Node 应用而言,信号是被看成事件发送给 Node 进程的,进程接收到 SIGTERMSIGINT 事件有默认回调,官方文档是这么描述的:

'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 进程管理很好的选择。

此文已由腾讯云+社区在各渠道发布

获取更多新鲜技术干货,能够关注咱们腾讯云技术社区-云加社区官方号及知乎机构号

相关文章
相关标签/搜索