您好,登錄后才能下訂單哦!
這篇文章主要介紹“導(dǎo)致Node.js進(jìn)程退出的情況是什么”的相關(guān)知識(shí),小編通過(guò)實(shí)際案例向大家展示操作過(guò)程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望這篇“導(dǎo)致Node.js進(jìn)程退出的情況是什么”文章能幫助大家解決問(wèn)題。
在我們的服務(wù)發(fā)布后,難免會(huì)被運(yùn)行環(huán)境(如容器、pm2 等)調(diào)度、升級(jí)服務(wù)導(dǎo)致重啟、各種異常導(dǎo)致進(jìn)程崩潰;一般情況下,運(yùn)行環(huán)境都有對(duì)服務(wù)進(jìn)程的健康監(jiān)測(cè),在進(jìn)程異常時(shí),會(huì)重新拉起進(jìn)程,在升級(jí)時(shí),也有滾動(dòng)升級(jí)的策略。但運(yùn)行環(huán)境的調(diào)度策略是把我們服務(wù)的進(jìn)程當(dāng)成黑盒來(lái)處理的,不會(huì)管服務(wù)進(jìn)程內(nèi)部的運(yùn)行情況,因此需要我們的服務(wù)進(jìn)程主動(dòng)感知運(yùn)行環(huán)境的調(diào)度動(dòng)作,然后做一些退出的清理動(dòng)作。
因此我們今天就是梳理各種可能導(dǎo)致 Node.js 進(jìn)程退出的情況,以及我們可以通過(guò)監(jiān)聽(tīng)這些進(jìn)程退出事件做哪些事情。
原理
一個(gè)進(jìn)程要退出,無(wú)非就是兩種情況,一是進(jìn)程自己主動(dòng)退出,另外就是收到系統(tǒng)信號(hào),要求進(jìn)程退出。
系統(tǒng)信號(hào)通知退出
在 Node.js 官方文檔 中列出了常見(jiàn)的系統(tǒng)信號(hào),我們主要關(guān)注幾個(gè):
SIGHUP:不通過(guò) ctrl+c 停止進(jìn)程,而是直接關(guān)閉命令行終端,會(huì)觸發(fā)該信號(hào)
SIGINT:按下 ctrl+c 停止進(jìn)程時(shí)觸發(fā);pm2 重啟或者停止子進(jìn)程時(shí),也會(huì)向子進(jìn)程發(fā)送該信號(hào)
SIGTERM:一般用于通知進(jìn)程優(yōu)雅退出,如 k8s 刪除 pod 時(shí),就會(huì)向 pod 發(fā)送 SIGTERM 信號(hào),pod 可以在超時(shí)時(shí)間內(nèi)(默認(rèn) 30s)做一些退出清理動(dòng)作
SIGBREAK:在 window 系統(tǒng)上,按下 ctrl+break 會(huì)觸發(fā)該信號(hào)
SIGKILL:強(qiáng)制退出進(jìn)程,進(jìn)程無(wú)法做任何清理動(dòng)作,執(zhí)行命令 kill -9 pid,進(jìn)程會(huì)收到該信號(hào)。k8s 刪除 pod 時(shí),如果超過(guò) 30s,pod 還沒(méi)退出,k8s 會(huì)向 pod 發(fā)送 SIGKILL 信號(hào),立即退出 pod 進(jìn)程;pm2 在重啟或者停止進(jìn)程時(shí),如果超過(guò) 1.6s,進(jìn)程還沒(méi)退出,也會(huì)發(fā)送 SIGKILL 信號(hào)
在收到非強(qiáng)制退出信號(hào)時(shí),Node.js 進(jìn)程可以監(jiān)聽(tīng)退出信號(hào),做一些自定義的退出邏輯。比如我們寫了一個(gè) cli 工具,需要比較長(zhǎng)的時(shí)間執(zhí)行任務(wù),如果用戶在任務(wù)執(zhí)行完成前想要通過(guò) ctrl+c 退出進(jìn)程時(shí),可以提示用戶再等等:
const readline = require('readline'); process.on('SIGINT', () => { // 我們通過(guò) readline 來(lái)簡(jiǎn)單地實(shí)現(xiàn)命令行里面的交互 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('任務(wù)還沒(méi)執(zhí)行完,確定要退出嗎?', answer => { if (answer === 'yes') { console.log('任務(wù)執(zhí)行中斷,退出進(jìn)程'); process.exit(0); } else { console.log('任務(wù)繼續(xù)執(zhí)行...'); } rl.close(); }); }); // 模擬一個(gè)需要執(zhí)行 1 分鐘的任務(wù) const longTimeTask = () => { console.log('task start...'); setTimeout(() => { console.log('task end'); }, 1000 * 60); }; longTimeTask();
實(shí)現(xiàn)效果如下,每次按下 ctrl + c 都會(huì)提示用戶:
進(jìn)程主動(dòng)退出
Node.js 進(jìn)程主動(dòng)退出,主要包含下面幾種情況:
代碼執(zhí)行過(guò)程中觸發(fā)了未捕獲的錯(cuò)誤,可以通過(guò) process.on('uncaughtException') 監(jiān)聽(tīng)這種情況
代碼執(zhí)行過(guò)程中觸發(fā)了未處理的 promise rejection(Node.js v16 開(kāi)始會(huì)導(dǎo)致進(jìn)程退出),可以通過(guò) process.on('unhandledRejection') 監(jiān)聽(tīng)這種情況
EventEmitter 觸發(fā)了未監(jiān)聽(tīng)的 error 事件
代碼中主動(dòng)調(diào)用 process.exit 函數(shù)退出進(jìn)程,可以通過(guò) process.on('exit') 監(jiān)聽(tīng)
Node.js 的事件隊(duì)列為空,可簡(jiǎn)單認(rèn)為沒(méi)有需要執(zhí)行的代碼了,可以通過(guò) process.on('exit') 監(jiān)聽(tīng)
我們知道 pm2 有守護(hù)進(jìn)程的效果,在你的進(jìn)程發(fā)生錯(cuò)誤退出時(shí),pm2 會(huì)重啟你的進(jìn)程,我們也在 Node.js 的 cluster 模式下,實(shí)現(xiàn)一個(gè)守護(hù)子進(jìn)程的效果(實(shí)際上 pm2 也是類似的邏輯):
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; const process = require('process'); // 主進(jìn)程代碼 if (cluster.isMaster) { console.log(`啟動(dòng)主進(jìn)程: ${process.pid}`); // 根據(jù) cpu 核數(shù),創(chuàng)建工作進(jìn)程 for (let i = 0; i < numCPUs; i++) { cluster.fork(); } // 監(jiān)聽(tīng)工作進(jìn)程退出事件 cluster.on('exit', (worker, code, signal) => { console.log(`工作進(jìn)程 ${worker.process.pid} 退出,錯(cuò)誤碼: ${code || signal}, 重啟中...`); // 重啟子進(jìn)程 cluster.fork(); }); } // 工作進(jìn)程代碼 if (cluster.isWorker) { // 監(jiān)聽(tīng)未捕獲錯(cuò)誤事件 process.on('uncaughtException', error => { console.log(`工作進(jìn)程 ${process.pid} 發(fā)生錯(cuò)誤`, error); process.emit('disconnect'); process.exit(1); }); // 創(chuàng)建 web server // 各個(gè)工作進(jìn)程都會(huì)監(jiān)聽(tīng)端口 8000(Node.js 內(nèi)部會(huì)做處理,不會(huì)導(dǎo)致端口沖突) http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(8000); console.log(`啟動(dòng)工作進(jìn)程: ${process.pid}`); }
應(yīng)用實(shí)踐
上面分析了 Node.js 進(jìn)程退出的各種情況,現(xiàn)在我們來(lái)做一個(gè)監(jiān)聽(tīng)進(jìn)程退出的工具,在 Node.js 進(jìn)程退出時(shí),允許使用方執(zhí)行自己的退出邏輯:
// exit-hook.js // 保存需要執(zhí)行的退出任務(wù) const tasks = []; // 添加退出任務(wù) const addExitTask = fn => tasks.push(fn); const handleExit = (code, error) => { // ...handleExit 的實(shí)現(xiàn)見(jiàn)下面 }; // 監(jiān)聽(tīng)各種退出事件 process.on('exit', code => handleExit(code)); // 按照 POSIX 的規(guī)范,我們用 128 + 信號(hào)編號(hào) 得到最終的退出碼 // 信號(hào)編號(hào)參考下面的圖片,大家可以在 linux 系統(tǒng)下執(zhí)行 kill -l 查看所有的信號(hào)編號(hào) process.on('SIGHUP', () => handleExit(128 + 1)); process.on('SIGINT', () => handleExit(128 + 2)); process.on('SIGTERM', () => handleExit(128 + 15)); // windows 下按下 ctrl+break 的退出信號(hào) process.on('SIGBREAK', () => handleExit(128 + 21)); // 退出碼 1 代表未捕獲的錯(cuò)誤導(dǎo)致進(jìn)程退出 process.on('uncaughtException', error => handleExit(1, error)); process.on('unhandledRejection', error => handleExit(1, error));
信號(hào)編號(hào):
接下來(lái)我們要實(shí)現(xiàn)真正的進(jìn)程退出函數(shù) handleExit,因?yàn)橛脩魝魅氲娜蝿?wù)函數(shù)可能是同步的,也可能是異步的;我們可以借助 process.nextTick 來(lái)保證用戶的同步代碼都已經(jīng)執(zhí)行完成,可以簡(jiǎn)單理解 process.nextTick 會(huì)在每個(gè)事件循環(huán)階段的同步代碼執(zhí)行完成后執(zhí)行(理解 process.nextTick);針對(duì)異步任務(wù),我們需要用戶調(diào)用 callback 來(lái)告訴我們異步任務(wù)已經(jīng)執(zhí)行完成了:
// 標(biāo)記是否正在退出,避免多次執(zhí)行 let isExiting = false; const handleExit = (code, error) => { if (isExiting) return; isExiting = true; // 標(biāo)記已經(jīng)執(zhí)行了退出動(dòng)作,避免多次調(diào)用 let hasDoExit = fasle; const doExit = () => { if (hasDoExit) return; hasDoExit = true process.nextTick(() => process.exit(code)) } // 記錄有多少個(gè)異步任務(wù) let asyncTaskCount = 0; // 異步任務(wù)結(jié)束后,用戶需要調(diào)用的回調(diào) let ayncTaskCallback = () => { process.nextTick(() => { asyncTaskCount-- if (asyncTaskCount === 0) doExit() }) } // 執(zhí)行所有的退出任務(wù) tasks.forEach(taskFn => { // 如果 taskFn 函數(shù)的參數(shù)個(gè)數(shù)大于 1,認(rèn)為傳遞了 callback 參數(shù),是一個(gè)異步任務(wù) if (taskFn.length > 1) { asyncTaskCount++ taskFn(error, ayncTaskCallback) } else { taskFn(error) } }); // 如果存在異步任務(wù) if (asyncTaskCount > 0) { // 超過(guò) 10s 后,強(qiáng)制退出 setTimeout(() => { doExit(); }, 10 * 1000) } else { doExit() } };
進(jìn)程優(yōu)雅退出
通常我們的 web server 在重啟、被運(yùn)行容器調(diào)度(pm2 或者 docker 等)、出現(xiàn)異常導(dǎo)致進(jìn)程退出時(shí),我們希望執(zhí)行退出動(dòng)作,如完成已經(jīng)連接到服務(wù)的請(qǐng)求響應(yīng)、清理數(shù)據(jù)庫(kù)連接、打印錯(cuò)誤日志、觸發(fā)告警等,做完退出動(dòng)作后,再退出進(jìn)程,我們可以使用剛才的進(jìn)程退出監(jiān)聽(tīng)工具實(shí)現(xiàn):
const http = require('http'); // 創(chuàng)建 web server const server = http.createServer((req, res) => { res.writeHead(200); res.end('hello world\n'); }).listen(8000); // 使用我們?cè)谏厦骈_(kāi)發(fā)的工具添加進(jìn)程退出任務(wù) addExitTask((error, callback) => { // 打印錯(cuò)誤日志、觸發(fā)告警、釋放數(shù)據(jù)庫(kù)連接等 console.log('進(jìn)程異常退出', error) // 停止接受新的請(qǐng)求 server.close((error) => { if (error) { console.log('停止接受新請(qǐng)求錯(cuò)誤', error) } else { console.log('已停止接受新的請(qǐng)求') } }) // 比較簡(jiǎn)單的做法是,等待一定的時(shí)間(這里我們等待 5s),讓存量請(qǐng)求執(zhí)行完畢 // 如果要完全保證所有請(qǐng)求都處理完畢,需要記錄每一個(gè)連接,在所有連接都釋放后,才執(zhí)行退出動(dòng)作 // 可以參考開(kāi)源庫(kù) https://github.com/sebhildebrandt/http-graceful-shutdown setTimout(callback, 5 * 1000) })
關(guān)于“導(dǎo)致Node.js進(jìn)程退出的情況是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。