NodeJS 进程是如何退出的

有几种因素可以导致 NodeJS 进程退出 。在这些因素中,有些是可预防的,比如代码抛出了一个异常;有些是不可预防的,比如内存耗尽 。process 这个全局变量是一个 Event Emitter 实例,如果进程优雅退出,process 会派发一个 exit 事件 。应用代码可以监听这个事件,来做最后的清理工作 。
【NodeJS 进程是如何退出的】下面的表格列举了可以导致进程退出的因素 。
操作举例手动退出process.exit(1)未捕获的异常throw new Error()未处理的 promise rejectionPromise.reject()未处理的 error 事件EventEmitter#emit('error')未处理的信号kill <PROCESS_ID>主动退出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 Errorsprocess.exit() 在应用启动配置检查等场景中非常有用,但是在处理运行时异常时,它并不适用,我们需要其他的工具 。
比如当应用在处理一个 HTTP 请求时,一个错误不应该导致进程终止,相反,我们应该返回一个含有错误信息的响应 。
Error 类可以包含描述错误发生的详细信息的数据,比如调用堆栈和错误文本 。通常我们会定义特定场景的 XXXError,这些 XXXError 都继承制 Error 类 。
当我们使用 throw 关键字,或者代码逻辑出错时,一个错误就会被抛出 。此时,系统调用栈会释放,每个函数会退出,直到遇到一个 包裹了当前调用的 try/catch 语句 。如果没有 try/catch 语句,则这个错误会被认为是未捕获的异常 。
通常,在 NodeJS 应用中,我们会给 Error 类定义一个 code 属性,作为用来描述具体错误的错误码,这么做的优点是可以使错误码保持唯一,同时还能使得错误码是可读的 。同时,我们也可以配合 message 属性来描述具体的错误信息 。
当一个未捕获的异常抛出时,控制台会打印调用堆栈,同时进程退出,退出状态码为 1.
/tmp/foo.js:1throw new TypeError('invalid foo');^Error: invalid fooat Object.<anonymous> (/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 noat Object.<anonymous> (/tmp/reject.js:1:16)... TRUNCATED ...at internal/main/run_main_module.js:17:47(node:52298) UnhandledPromiseRejectionWarning: Unhandled promiserejection. This error originated either by throwing inside of anasync function without a catch block, or by rejecting a promisewhich 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:306throw err; // Unhandled 'error' event^Error [ERR_UNHANDLED_ERROR]: Unhandled error. (undefined)at EventEmitter.emit (events.js:304:17)at Object.<anonymous> (/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 默认行为信号的含义SIGHUP1Yes退出父命令行被关闭SIGINT2Yes退出命令行尝试中断,即 Ctrl + CSIGQUIT3Yes退出命令行尝试退出,即 Ctrl + ZSIGKILL9No退出强制进程退出SIGUSR110Yes启动调试器用户自定义信号SIGUSR212Yes退出用户自定义信号SIGTERM15Yes退出进程优雅的退出SIGSTOP19No退出进程被强制停止这表格里,是否可处理表示这个信号是否可被进程接收并被处理 。NodeJS 默认行为表示进程在接收到这个信号以后默认执行的动作 。
我们可以通过如下方式来监听这些信号 。
#!/usr/bin/env nodeconsole.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 <PROCESS_ID>通过新起的命令行,我们向原来的那个程序进程发送了一个 SIGHUP 信号,原来的命令行窗口中会打印一行接收到了 SIGHUP 信号的日志信息 。
在 NodeJS 代码中,进程也可以给其他进程发送信号 。比如:
node -e "process.kill(<PROCESS_ID>, 'SIGHUP')"这段代码同样会在第一个命令行窗口中输出一行接收到了 SIGHUP 信号的日志 。
如果我们要让第一个命令行窗口的进程退出,则可以通过下面的命令来实现 。
kill -9 <PROCESS_ID>在 NodeJS 中,信号通常被用作控制进程优雅的退出 。比如,在 Kubernetes 中,当一个 pod 要退出时,k8s 会像 pod 内的进程发送一个 SIGTERM 的信号,同时启动一个 30 秒的定时器 。应用程序有 30 秒的时间来关闭连接、保存数据等 。如果 30 秒之后进程依然存活,k8s 会再发送一个 SIGKILL 来强制关闭进程 。
小结本文讲述了可以导致进程退出的几种因素,分别是:

  • 主动退出
  • 未捕获的异常、未处理的 promise rejection、未处理的 Event Emitter error 事件
  • 系统信号
欢迎关注公众号“众里千寻”或者在我的网站浏览更多更系统的信息 。