详细谈谈NodeJS进程是如何退出的

前言

有几种因素可以导致 NodeJS 进程退出。在这些因素中,有些是可预防的,比如代码抛出了一个异常;有些是不可预防的,比如内存耗尽。process 这个全局变量是一个 Event Emitter 实例,如果进程优雅退出,process 会派发一个 exit 事件。应用代码可以监听这个事件,来做最后的清理工作。

下面的表格列举了可以导致进程退出的因素。

操作 举例
手动退出 process.exit(1)
未捕获的异常 throw new Error()
未处理的 promise rejection Promise.reject()
未处理的 error 事件 EventEmitter#emit('error')
未处理的信号 kill

主动退出

process.exit(code) 是最直接的结束进程的方法。code 参数是可选的,可以为 0 ~ 255 之间任何数字,默认为 0。0 表示进程执行成功,非 0 数字表示进程执行失败。

当 process.exit() 被使用时,控制台不会有任何输出,如果我们想在进程推出的时候像控制台输出一些错误说明信息,则需要在调用之前显示的输出错误信息。

node -e "process.exit(42)"
echo $?

上面的代码直接退出了 NodeJS 进程,命令行没有任何输出信息。用户在遭遇进程退出的时候,无法获取有效的错误信息。

function checkConfig(config) {
  if (!config.host) {
    console.error("Configuration is missing 'host' parameter!");
    process.exit(1);
  }
}

在上面的代码中,我们在进程退出之前输出的明确的错误信息。

process.exit() 的功能非常强大,但是我们不应该在工具库中使用。如果在工具库中遇到的错误,我们应该以异常的形式抛出,从而让应用代码决定如何处理这个错误。

Exceptions, Rejections 和 Emitted Errors

process.exit() 在应用启动配置检查等场景中非常有用,但是在处理运行时异常时,它并不适用,我们需要其他的工具。

比如当应用在处理一个 HTTP 请求时,一个错误不应该导致进程终止,相反,我们应该返回一个含有错误信息的响应。

Error 类可以包含描述错误发生的详细信息的数据,比如调用堆栈和错误文本。通常我们会定义特定场景的 XXXError,这些 XXXError 都继承制 Error 类。

当我们使用 throw 关键字,或者代码逻辑出错时,一个错误就会被抛出。此时,系统调用栈会释放,每个函数会退出,直到遇到一个 包裹了当前调用的 try/catch 语句。如果没有 try/catch 语句,则这个错误会被认为是未捕获的异常。

通常,在 NodeJS 应用中,我们会给 Error 类定义一个 code 属性,作为用来描述具体错误的错误码,这么做的优点是可以使错误码保持唯一,同时还能使得错误码是可读的。同时,我们也可以配合 message 属性来描述具体的错误信息。

当一个未捕获的异常抛出时,控制台会打印调用堆栈,同时进程退出,退出状态码为 1.

/tmp/foo.js:1
throw new TypeError('invalid foo');
^
Error: invalid foo
    at Object. (/tmp/foo.js:2:11)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47

这段控制台输出信息说明,错误发生在 foo.js 中的第 2 行第 11 列。

全局变量 process 是个 Event Emitter 实例,可以通过监听 uncaughtException 事件来处理这些未捕获异常。下面的代码展示了如何使用:

const logger = require("./lib/logger.js");
process.on("uncaughtException", (error) => {
  logger.send("An uncaught exception has occured", error, () => {
    console.error(error);
    process.exit(1);
  });
});

Promise Rejection 与抛出异常类似。我们可以通过调用 reject() 函数或者在 async 函数中抛出异常来是的 promise 到达 rejected 状态。下面的两段代码功能是相似的。

Promise.reject(new Error("oh no"));

(async () => {
  throw new Error("oh no");
})();

目前,在 NodeJS 14 中,Promise Rejection 不会导致进程退出,在后续的版本中,Promise Rejection 可能会导致进程退出。

下面是一段未捕获的 Promise Rejection 的控制台输出样例。

(node:52298) UnhandledPromiseRejectionWarning: Error: oh no
    at Object. (/tmp/reject.js:1:16)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47
(node:52298) UnhandledPromiseRejectionWarning: Unhandled promise
  rejection. This error originated either by throwing inside of an
  async function without a catch block, or by rejecting a promise
  which was not handled with .catch().

我们可以通过监听 unhandledRejection 事件来处理未捕获的 Rejection. 样例代码如下:

process.on("unhandledRejection", (reason, promise) => {});

Event Emitter 是 NodeJS 中的基础模块,应用广泛。当 Event Emitter 的 error 事件未被处理时,Event Emitter 就会抛出一个错误,同时会导致进程退出。下面是一个 Event Emitter error 的控制台输出。

events.js:306
    throw err; // Unhandled 'error' event
    ^
Error [ERR_UNHANDLED_ERROR]: Unhandled error. (undefined)
    at EventEmitter.emit (events.js:304:17)
    at Object. (/tmp/foo.js:1:40)
    ... TRUNCATED ...
    at internal/main/run_main_module.js:17:47 {
  code: 'ERR_UNHANDLED_ERROR',
  context: undefined
}

因此,我们在使用 Event Emitter 的时候,要确保监听了 error 事件,这样在发生错误的时候,可以使得应用能够处理这些错误,避免奔溃。

信号

信号是操作信息提供了进程间通信机制。信号通常是一个数字,同时也可以使用一个字符串来标识。比如 SIGKILL 标识数字 9。不同的操作系统对信号的定义不同。下面表格里罗列的是基本通用的信号定义。

名称 数字 是否可处理 NodeJS 默认行为 信号的含义
SIGHUP 1 Yes 退出 父命令行被关闭
SIGINT 2 Yes 退出 命令行尝试中断,即 Ctrl + C
SIGQUIT 3 Yes 退出 命令行尝试退出,即 Ctrl + Z
SIGKILL 9 No 退出 强制进程退出
SIGUSR1 10 Yes 启动调试器 用户自定义信号
SIGUSR2 12 Yes 退出 用户自定义信号
SIGTERM 15 Yes 退出 进程优雅的退出
SIGSTOP 19 No 退出 进程被强制停止

这表格里,是否可处理表示这个信号是否可被进程接收并被处理。NodeJS 默认行为表示进程在接收到这个信号以后默认执行的动作。

我们可以通过如下方式来监听这些信号。

#!/usr/bin/env node
console.log(`Process ID: ${process.pid}`);
process.on("SIGHUP", () => console.log("Received: SIGHUP"));
process.on("SIGINT", () => console.log("Received: SIGINT"));
setTimeout(() => {}, 5 * 60 * 1000); // keep process alive

在一个命令行窗口中运行这段代码,然后按下 Ctrl + C,此时进程不会退出,而是会在控制台打印一行接收到了 SIGINT 信号的日志信息。新起一个命令行窗口,执行如下命令,PROCESS_ID 为上面程序输出的进程 ID。

kill -s SIGHUP 

通过新起的命令行,我们向原来的那个程序进程发送了一个 SIGHUP 信号,原来的命令行窗口中会打印一行接收到了 SIGHUP 信号的日志信息。

在 NodeJS 代码中,进程也可以给其他进程发送信号。比如:

node -e "process.kill(, 'SIGHUP')"

这段代码同样会在第一个命令行窗口中输出一行接收到了 SIGHUP 信号的日志。

如果我们要让第一个命令行窗口的进程退出,则可以通过下面的命令来实现。

kill -9 

在 NodeJS 中,信号通常被用作控制进程优雅的退出。比如,在 Kubernetes 中,当一个 pod 要退出时,k8s 会像 pod 内的进程发送一个 SIGTERM 的信号,同时启动一个 30 秒的定时器。应用程序有 30 秒的时间来关闭连接、保存数据等。如果 30 秒之后进程依然存活,k8s 会再发送一个 SIGKILL 来强制关闭进程。

小结

本文讲述了可以导致进程退出的几种因素,分别是:

  • 主动退出
  • 未捕获的异常、未处理的 promise rejection、未处理的 Event Emitter error 事件
  • 系统信号

到此这篇关于NodeJS进程是如何退出的文章就介绍到这了,更多相关NodeJS进程退出内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(详细谈谈NodeJS进程是如何退出的)