詳細談談NodeJS進程是如何退出的
前言
有幾種因素可以導致 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進程是如何退出的文章就介紹到這瞭,更多相關NodeJS進程退出內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Node端異常捕獲的實現方法
- JS異步代碼單元測試之神奇的Promise
- 淺析Promise的介紹及基本用法
- JavaScript異步隊列進行try catch時的問題解決
- 詳解JavaScript錯誤捕獲