1. 程式人生 > 其它 >NodeJS 程序是如何退出的

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 事件
  • 系統訊號

歡迎關注公眾號“眾裡千尋”或者在我的網站瀏覽更多更系統的資訊。