今天小编给大家分享一下Node.js进程退出的原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
一个进程要退出,无非就是两种情况,一是进程自己主动退出,另外就是收到系统信号,要求进程退出。
在 Node.js 官方文档 中列出了常见的系统信号,我们主要关注几个:
SIGHUP:不通过 ctrl+c 停止进程,而是直接关闭命令行终端,会触发该信号
SIGINT:按下 ctrl+c 停止进程时触发;pm2 重启或者停止子进程时,也会向子进程发送该信号
SIGTERM:一般用于通知进程优雅退出,如 k8s 删除 pod 时,就会向 pod 发送 SIGTERM 信号,pod 可以在超时时间内(默认 30s)做一些退出清理动作
SIGBREAK:在 window 系统上,按下 ctrl+break 会触发该信号
SIGKILL:强制退出进程,进程无法做任何清理动作,执行命令 kill -9 pid,进程会收到该信号。k8s 删除 pod 时,如果超过 30s,pod 还没退出,k8s 会向 pod 发送 SIGKILL 信号,立即退出 pod 进程;pm2 在重启或者停止进程时,如果超过 1.6s,进程还没退出,也会发送 SIGKILL 信号
在收到非强制退出信号时,Node.js 进程可以监听退出信号,做一些自定义的退出逻辑。比如我们写了一个 cli 工具,需要比较长的时间执行任务,如果用户在任务执行完成前想要通过 ctrl+c 退出进程时,可以提示用户再等等:
const readline = require('readline');
process.on('SIGINT', () => {
// 我们通过 readline 来简单地实现命令行里面的交互
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('任务还没执行完,确定要退出吗?', answer => {
if (answer === 'yes') {
console.log('任务执行中断,退出进程');
process.exit(0);
} else {
console.log('任务继续执行...');
}
rl.close();
});
});
// 模拟一个需要执行 1 分钟的任务
const longTimeTask = () => {
console.log('task start...');
setTimeout(() => {
console.log('task end');
}, 1000 * 60);
};
longTimeTask();
实现效果如下,每次按下 ctrl + c 都会提示用户:
Node.js 进程主动退出,主要包含下面几种情况:
代码执行过程中触发了未捕获的错误,可以通过 process.on('uncaughtException') 监听这种情况
代码执行过程中触发了未处理的 promise rejection(Node.js v16 开始会导致进程退出),可以通过 process.on('unhandledRejection') 监听这种情况
EventEmitter 触发了未监听的 error 事件
代码中主动调用 process.exit 函数退出进程,可以通过 process.on('exit') 监听
Node.js 的事件队列为空,可简单认为没有需要执行的代码了,可以通过 process.on('exit') 监听
我们知道 pm2 有守护进程的效果,在你的进程发生错误退出时,pm2 会重启你的进程,我们也在 Node.js 的 cluster 模式下,实现一个守护子进程的效果(实际上 pm2 也是类似的逻辑):
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
const process = require('process');
// 主进程代码
if (cluster.isMaster) {
console.log(`启动主进程: ${process.pid}`);
// 根据 cpu 核数,创建工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 监听工作进程退出事件
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 退出,错误码: ${code || signal}, 重启中...`);
// 重启子进程
cluster.fork();
});
}
// 工作进程代码
if (cluster.isWorker) {
// 监听未捕获错误事件
process.on('uncaughtException', error => {
console.log(`工作进程 ${process.pid} 发生错误`, error);
process.emit('disconnect');
process.exit(1);
});
// 创建 web server
// 各个工作进程都会监听端口 8000(Node.js 内部会做处理,不会导致端口冲突)
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
console.log(`启动工作进程: ${process.pid}`);
}
上面分析了 Node.js 进程退出的各种情况,现在我们来做一个监听进程退出的工具,在 Node.js 进程退出时,允许使用方执行自己的退出逻辑:
// exit-hook.js
// 保存需要执行的退出任务
const tasks = [];
// 添加退出任务
const addExitTask = fn => tasks.push(fn);
const handleExit = (code, error) => {
// ...handleExit 的实现见下面
};
// 监听各种退出事件
process.on('exit', code => handleExit(code));
// 按照 POSIX 的规范,我们用 128 + 信号编号 得到最终的退出码
// 信号编号参考下面的图片,大家可以在 linux 系统下执行 kill -l 查看所有的信号编号
process.on('SIGHUP', () => handleExit(128 + 1));
process.on('SIGINT', () => handleExit(128 + 2));
process.on('SIGTERM', () => handleExit(128 + 15));
// windows 下按下 ctrl+break 的退出信号
process.on('SIGBREAK', () => handleExit(128 + 21));
// 退出码 1 代表未捕获的错误导致进程退出
process.on('uncaughtException', error => handleExit(1, error));
process.on('unhandledRejection', error => handleExit(1, error));
信号编号:
接下来我们要实现真正的进程退出函数 handleExit,因为用户传入的任务函数可能是同步的,也可能是异步的;我们可以借助 process.nextTick 来保证用户的同步代码都已经执行完成,可以简单理解 process.nextTick 会在每个事件循环阶段的同步代码执行完成后执行(理解 process.nextTick);针对异步任务,我们需要用户调用 callback 来告诉我们异步任务已经执行完成了:
// 标记是否正在退出,避免多次执行
let isExiting = false;
const handleExit = (code, error) => {
if (isExiting) return;
isExiting = true;
// 标记已经执行了退出动作,避免多次调用
let hasDoExit = fasle;
const doExit = () => {
if (hasDoExit) return;
hasDoExit = true
process.nextTick(() => process.exit(code))
}
// 记录有多少个异步任务
let asyncTaskCount = 0;
// 异步任务结束后,用户需要调用的回调
let ayncTaskCallback = () => {
process.nextTick(() => {
asyncTaskCount--
if (asyncTaskCount === 0) doExit()
})
}
// 执行所有的退出任务
tasks.forEach(taskFn => {
// 如果 taskFn 函数的参数个数大于 1,认为传递了 callback 参数,是一个异步任务
if (taskFn.length > 1) {
asyncTaskCount++
taskFn(error, ayncTaskCallback)
} else {
taskFn(error)
}
});
// 如果存在异步任务
if (asyncTaskCount > 0) {
// 超过 10s 后,强制退出
setTimeout(() => {
doExit();
}, 10 * 1000)
} else {
doExit()
}
};
至此,我们的进程退出监听工具就完成了,完整的实现可以查看这个开源库 async-exit-hook
通常我们的 web server 在重启、被运行容器调度(pm2 或者 docker 等)、出现异常导致进程退出时,我们希望执行退出动作,如完成已经连接到服务的请求响应、清理数据库连接、打印错误日志、触发告警等,做完退出动作后,再退出进程,我们可以使用刚才的进程退出监听工具实现:
const http = require('http');
// 创建 web server
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
// 使用我们在上面开发的工具添加进程退出任务
addExitTask((error, callback) => {
// 打印错误日志、触发告警、释放数据库连接等
console.log('进程异常退出', error)
// 停止接受新的请求
server.close((error) => {
if (error) {
console.log('停止接受新请求错误', error)
} else {
console.log('已停止接受新的请求')
}
})
// 比较简单的做法是,等待一定的时间(这里我们等待 5s),让存量请求执行完毕
// 如果要完全保证所有请求都处理完毕,需要记录每一个连接,在所有连接都释放后,才执行退出动作
// 可以参考开源库 https://github.com/sebhildebrandt/http-graceful-shutdown
setTimout(callback, 5 * 1000)
})
以上就是“Node.js进程退出的原理是什么”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注亿速云行业资讯频道。
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。