溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

導(dǎo)致Node.js進(jìn)程退出的情況是什么

發(fā)布時(shí)間:2022-04-06 10:58:33 來(lái)源:億速云 閱讀:214 作者:iii 欄目:web開(kāi)發(fā)

這篇文章主要介紹“導(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)題。

導(dǎo)致Node.js進(jì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ì)提示用戶:

導(dǎo)致Node.js進(jìn)程退出的情況是什么

進(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):

導(dǎo)致Node.js進(jìn)程退出的情況是什么

接下來(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)。

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI