
如何快速入門VUE3.0:進入學習
在我們的服務發布後,難免會被運行環境(如容器、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 worldn');
}).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 worldn');
}).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 等工具能夠在進程異常退出時,不停地拉起進程,保證服務的可用性,但我們也應該在程式碼中主動感知進程的異常或者被調度的情況,從而能夠更早發現問題。