有几种因素可以导致 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 事件
  • 系统信号

欢迎关注公众号“众里千寻”或者在我的网站浏览更多更系统的信息。

NodeJS 进程是如何退出的的更多相关文章

  1. nodejs(三) --- nodejs进程与子进程

    嗯,对于node的学习还远远不够,这里先做一个简单的api的记录,后续深入学习. 第一部分:nodejs中的全局对象之process进程对象 在node中的全局对象是global,相当于浏览器中的wi ...

  2. nodejs进程管理

    NodeJS可以感知和控制自身进程的运行环境和状态,也可以创建子进程并与其协同工作,这使得NodeJS可以把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用. 我们已经知道了Node ...

  3. [批处理]守护NodeJS进程

    背景: 日常进行CI过程中,使用NodeJs方式:GIT更新->检测是否需要编译->调用IncrediBuilder编译->读取编译日志判断是否通过->调用7z打包 问题: 持 ...

  4. 使用PM2管理nodejs进程分享

    摘要:pm2 是一个带有负载均衡功能的Node应用的进程管理器.本文主要介绍了详解使用PM2管理nodejs进程,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧,希望能帮助 ...

  5. 【旧文章搬运】如何从EPROCESS辨别一个进程是否已退出

    原文发表于百度空间,2008-7-31========================================================================== 前面已经通过 ...

  6. update_notifier 造成nodejs进程数量增长的问题

    最近运维老大j哥找到我说了一个事儿:某私有化部署的线上环境nodejs进程数量多达1000+,对比公版线上环境的66个进程数显得十分诡异.并且单个nodejs进程所占用swap空间也较大,也不释放空间 ...

  7. Linux_CentOS下搭建Nodejs 生产环境-以及nodejs进程管理器pm2的使用

    nodejs安装:https://www.cnblogs.com/loaderman/p/11596661.html nodejs 进程管理器 pm2 的使用 PM2 是一款非常优秀的 Node 进程 ...

  8. VS2017 CMD多出 “进程 6420)已退出,返回代码为: 0”的内容

    执行cmd, 命令行多出如下内容 xxxx\project.exe (进程 6420)已退出,返回代码为: 0. VS 取消设置方式: 工具->选项->调试-->常规     拉到最 ...

  9. IDEA惊天bug:进程已结束,退出代码-1073741819 (0xC0000005)

    由于昨天要写的文章没有写完,于是今天早上我四点半就"自然醒"了,心里面有事,睡觉也不安稳.洗漱完毕后,我打开电脑,正襟危坐,摆出一副要干架的态势,不能再拖了. 要写的文章中涉及到一 ...

随机推荐

  1. 微信小程序使用同声传译实现语音识别功能

    我使用同声传译语音识别功能是为了实现微信小程序首页的语音搜索功能,如果你也是那么恭喜你,你可以ctrl+c.ctrl+v再改一改,如果你不是那么你也不要着急的走可以看完我的文章会对你有所帮助! 首先是 ...

  2. Apple macOS 下载汇总

    macOS Big Sur 11,macOS Catalina 10.15,macOS Mojave 10.14,macOS High Sierra 10.13,macOS Sierra 10.12 ...

  3. [leetcode] 39. 组合总和(Java)(dfs、递归、回溯)

    39. 组合总和 直接暴力思路,用dfs+回溯枚举所有可能组合情况.难点在于每个数可取无数次. 我的枚举思路是: 外层枚举答案数组的长度,即枚举解中的数字个数,从1个开始,到target/ min(c ...

  4. openresty 学习笔记五:访问RabbitMQ消息队列

    openresty 学习笔记五:访问RabbitMQ消息队列 之前通过比较选择,决定采用RabbitMQ这种消息队列来做中间件,目的舒缓是为了让整个架构的瓶颈环节.这里是做具体实施,用lua访问Rab ...

  5. CUDA统一内存分析

    CUDA统一内存分析 PascalMIG 如 NVIDIA Titan X 和 NVIDIA Tesla P100 是第一个包含页 GPUs 定额引擎的 GPUs ,它是统一内存页错误处理和 MIG ...

  6. SQL Parameter参数的用法

    SqlParameter 类 表示 SqlCommand 的参数,也可以是它到 DataSet 列的映射. 无法继承此类. 命名空间:  System.Data.SqlClient 程序集:  Sys ...

  7. 打造住院新体验,GVS智慧病房有何独到之处?

    3月26-28日,由广东省医院协会主办的"2021第二届广东省医院建设大会暨医院建筑与装备展览会"在广州琶洲国际采购中心盛大举办,来自全国各地的医院代表及企事业单位代表4000余人 ...

  8. MySQL压缩包下载解压安装步骤

    MySQL官网下载地址:https://downloads.mysql.com/archives/community/ 1.选择自己需要的版本,本教程是mysql-5.7.29-winx64版本 2. ...

  9. UiPath中恢复依赖项失败的解决方法

    目录 序言 正文 什么是依赖包? 如何查看项目使用了哪些版本的依赖包? 一.项目内查看 二.查看项目的 JSON 文件 问题根源 解决方法 一.「等」字诀 二.切换网络环境(根治) 三.手动复制依赖包 ...

  10. Pytorch Dataset和Dataloader 学习笔记(二)

    Pytorch Dataset & Dataloader Pytorch框架下的工具包中,提供了数据处理的两个重要接口,Dataset 和 Dataloader,能够方便的使用和加载自己的数据 ...