【译】正确地关闭 Node.js app

5,193 阅读3分钟

本文是在阮一峰老师的科技爱好者周刊:第 96 期中介绍的文章。平时都是 Ctrl + C 简单粗暴来解决,因此来看一下正确关闭 Node.js 程序的姿势。

英文水平有限,如有错误之处还请指正。


正文:正确地关闭 Node.js app

原文地址: Shutdown correctly Node.js apps

正确地关闭应用,并防止其处理新的请求是非常重要的。我将以一个服务器应用为例子展开。

const http = require('http');
const server = http.createServer(function (req, res) {
  setTimeout(function () {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
  }, 4000);
}).listen(9090, function (err) {
  console.log('listening http://localhost:9090/');
  console.log('pid is ' + process.pid);
});

正如我们所见,服务器没有正确地被关闭,并且请求也没有得到正确的处理。那么为了处理这个问题,首先我们需要明白 Node.js 是如何关闭进程的。

当关闭进程时,进程会收到一个信号。这些信号有许多种类,在这里我们主要关心下面三种。

  • SIGINT: 来自键盘 Ctrl + C 的组合(中断进程)
  • SIGQUIT: 来自键盘 Ctrl + \(关闭进程)这个操作同时也会生成一个 core 文件
  • SIGTERM: 通过系统操作退出(例如使用 kill 命令)

当进程收到这些信号时,Node.js 会触发事件。因此我们就可以对这些事件编写对应的方法。在下面这个例子中我们将关闭服务器,它会处理完那些被挂起的请求并且不会再接受新的请求。

// ...
function handleExit(signal) {
  console.log(`Received ${signal}. Close my server properly.`)
  server.close(function () {
    process.exit(0);
  });
}
process.on('SIGINT', handleExit);
process.on('SIGQUIT', handleExit);
process.on('SIGTERM', handleExit);

现在我们可以很好的处理请求并正确地关闭服务器。你可以在 Nairi Harutyunyan文章 中找到更多信息。文章对于如何正确地关闭带有数据库的服务器做了详细解释。

现在还有名为 Death 的 npm 包可以处理这些逻辑。

const ON_DEATH = require('death')
ON_DEATH(function(signal, err) {
  // clean up code here
})

有时这样还不够

最近遇到了这样的场景,我的服务器会接受一个请求来关闭服务器。接下来我会举一些例子。我为服务器订阅了一个 webhook。而这个 webhook 所接受的订阅有限。那么当我不想超出订阅数量时,那我就需要正确地关闭服务器并取消订阅。整个步骤如下:

  1. 向 webhook 发送取消订阅的请求
  2. webhook 则会向服务器发送确认收到取消订阅的请求
  3. 服务器经过校验,然后取消订阅

当进程的事件循环为空时,程序就会自动关闭。正如上面的 1、2 步所示,如果事件循环为空,则进程将会终止,我们也将无法成功取消订阅。

下面将是新的服务器代码:

const server = http.createServer(function (req, res) {
  const params = qs.decode(req.url.split("?")[1]);
  if (params.mode) {
    res.writeHead(200);
    res.write(params.challenge);
    res.end();
  } else {
    let body = "";
    req.on("data", chunk => {
      body += chunk;
    });
    req.on("end", () => {
      console.log('event', JSON.parse(body))
      res.writeHead(200);
      res.end();
    });
  }
}).listen(9090, function (err) {
  console.log('listening http://localhost:9090/');
  console.log('pid is ' + process.pid);
  fetch('http://localhost:3000/webhook?mode=subscribe&callback=http://localhost:9090')
});

这段代码里有两个变化。当我们在启动服务器时,会向 webhook 服务器发送一个订阅请求。

// ...
fetch('http://localhost:3000/webhook?mode=subscribe&callback=http://localhost:9090')
// ...

同时我们还在服务器处理请求的方法中添加一个 challenge 的 token,来确认表明 response 来自于 webhook。

// ...
if (params.mode) {
  res.writeHead(200);
  res.write(params.challenge);
  res.end();
} else {
 // ...
}
// ...

让我们再来修改一下 handleExit 方法,让它向 webhook 发送一个请求,告诉它取消订阅。

function handleExit(signal) {
  console.log(`Received ${signal}. Close my server properly.`)
  fetch('http://localhost:3000/webhook?mode=unsubscribe&callback=http://localhost:9090')
}

我们需要添加代码让服务器在 webhook 取消订阅时能够关闭。

// ...
if (params.mode) {
  res.writeHead(200);
  res.write(params.challenge);
  res.end();
  if (params.mode === 'unsubscribe') {
    server.close(function () {
      process.exit(0);
    });
  }
} else {
  // ...
}
// ...

当 webhook 确认取消订阅时,我们将关闭服务器,这样一来它就不会再处理新的请求并正常地退出。而这也是 Twitch API 中 webhook 的 subscription/unsubscription 的工作原理。

让我们看一下实际关闭服务器时的情况。我添加了一些 log 让其更容易理解。

如图所示,服务器并不能正常关闭。服务器的进程在收到 webhook 返回的请求之前就关闭了,因此 webhook 仍然会往服务器发送请求。

为了解决这个问题,我们需要防止 Node.js 进程退出。我们可以使用 process.stdin.resume 方法。这个方法会使进程暂停并覆盖默认方法,比如 Ctrl + C。

注:我自己跑了一下代码,即使不加 process.stdin.resume 也可以正常取消订阅,但如果不添加 handleExit,就会出现无法取消订阅的情况。

const http = require('http');
process.stdin.resume();
// ...

那么现在再来看一下?

现在的服务器会在处理完请求后退出。

我创建了一个 Git 的 repo 上面有文中所提到的代码。

希望能有所帮助。


欢迎关注我的公众号:此方的手账。一个与你分享生活、共同进步的公众号。除了前端还有高达系列的分享哦~