這篇文章聊聊Node的進程退出,介紹各種可能導致 Node.js 進程退出的情況,希望對大家有幫助!
在我們的服務發布後,難免會被運行環境(如容器、pm2 等)調度、升級服務導致重啟、各種異常導致進程崩潰;一般情況下,運作環境都有服務進程的健康監測,在進程異常時,會重新拉起進程,升級時,也有滾動升級的策略。但運行環境的調度策略是把我們服務的進程當成黑盒來處理的,不會管服務進程內部的運行情況,因此需要我們的服務進程主動感知運行環境的調度動作,然後做一些退出的清理動作。
因此我們今天就是梳理各種可能導致 Node.js 進程退出的情況,以及我們可以透過監聽這些進程退出事件做哪些事情。
原理
一個行程要退出,無非就是兩種情況,一是行程自己主動退出,另外就是收到系統訊號,要求進程退出。
系統訊號通知退出
在Node.js 官方文件 中列出了常見的系統訊號,我們主要關注幾個:
在收到非強制退出訊號時,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 進程主動退出,主要包含下面幾種情況:
我們知道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
https://github.com/darukjs/daruk-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 进程退出的各种情况心里有数了。在服务上线后,虽然 k8s、pm2 等工具能够在进程异常退出时,不停地拉起进程,保证服务的可用性,但我们也应该在代码中主动感知进程的异常或者被调度的情况,从而能够更早发现问题。
更多node相关知识,请访问:nodejs 教程!
以上是聊聊各種可能導致 Node.js 進程退出的情況的詳細內容。更多資訊請關注PHP中文網其他相關文章!