NodeJS 进程是如何退出的
有几种因素可以导致 NodeJS 进程退出。在这些因素中,有些是可预防的,比如代码抛出了一个异常;有些是不可预防的,比如内存耗尽。process
这个全局变量是一个 Event Emitter 实例,如果进程优雅退出,process
会派发一个 exit
事件。应用代码可以监听这个事件,来做最后的清理工作。
下面的表格列举了可以导致进程退出的因素。
操作 | 举例 |
---|---|
手动退出 | process.exit(1) |
未捕获的异常 | throw new Error() |
未处理的 promise rejection | Promise.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 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.<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 no
at Object.<anonymous> (/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.<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 默认行为 | 信号的含义 |
---|---|---|---|---|
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 <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 事件
- 系统信号
欢迎关注公众号“众里千寻”或者在我的网站浏览更多更系统的信息。