溫馨提示×

溫馨提示×

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

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

Node.js進(jìn)程退出的原理是什么

發(fā)布時間:2022-04-02 13:47:52 來源:億速云 閱讀:301 作者:iii 欄目:開發(fā)技術(shù)

今天小編給大家分享一下Node.js進(jìn)程退出的原理是什么的相關(guān)知識點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

    原理

    一個進(jìn)程要退出,無非就是兩種情況,一是進(jìn)程自己主動退出,另外就是收到系統(tǒng)信號,要求進(jìn)程退出。

    系統(tǒng)信號通知退出

    在 Node.js 官方文檔 中列出了常見的系統(tǒng)信號,我們主要關(guān)注幾個:

    • SIGHUP:不通過 ctrl+c 停止進(jìn)程,而是直接關(guān)閉命令行終端,會觸發(fā)該信號

    • SIGINT:按下 ctrl+c 停止進(jìn)程時觸發(fā);pm2 重啟或者停止子進(jìn)程時,也會向子進(jìn)程發(fā)送該信號

    • SIGTERM:一般用于通知進(jìn)程優(yōu)雅退出,如 k8s 刪除 pod 時,就會向 pod 發(fā)送 SIGTERM 信號,pod 可以在超時時間內(nèi)(默認(rèn) 30s)做一些退出清理動作

    • SIGBREAK:在 window 系統(tǒng)上,按下 ctrl+break 會觸發(fā)該信號

    • SIGKILL:強(qiáng)制退出進(jìn)程,進(jìn)程無法做任何清理動作,執(zhí)行命令 kill -9 pid,進(jìn)程會收到該信號。k8s 刪除 pod 時,如果超過 30s,pod 還沒退出,k8s 會向 pod 發(fā)送 SIGKILL 信號,立即退出 pod 進(jìn)程;pm2 在重啟或者停止進(jìn)程時,如果超過 1.6s,進(jìn)程還沒退出,也會發(fā)送 SIGKILL 信號

    在收到非強(qiáng)制退出信號時,Node.js 進(jìn)程可以監(jiān)聽退出信號,做一些自定義的退出邏輯。比如我們寫了一個 cli 工具,需要比較長的時間執(zhí)行任務(wù),如果用戶在任務(wù)執(zhí)行完成前想要通過 ctrl+c 退出進(jìn)程時,可以提示用戶再等等:

    const readline = require('readline');
    
    process.on('SIGINT', () => {
      // 我們通過 readline 來簡單地實(shí)現(xiàn)命令行里面的交互
      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
      });
      rl.question('任務(wù)還沒執(zhí)行完,確定要退出嗎?', answer => {
        if (answer === 'yes') {
          console.log('任務(wù)執(zhí)行中斷,退出進(jìn)程');
          process.exit(0);
        } else {
          console.log('任務(wù)繼續(xù)執(zhí)行...');
        }
        rl.close();
      });
    });
    
    // 模擬一個需要執(zhí)行 1 分鐘的任務(wù)
    const longTimeTask = () => {
      console.log('task start...');
      setTimeout(() => {
        console.log('task end');
      }, 1000 * 60);
    };
    
    longTimeTask();

    實(shí)現(xiàn)效果如下,每次按下 ctrl + c 都會提示用戶:

    Node.js進(jìn)程退出的原理是什么

    進(jìn)程主動退出

    Node.js 進(jìn)程主動退出,主要包含下面幾種情況:

    • 代碼執(zhí)行過程中觸發(fā)了未捕獲的錯誤,可以通過 process.on('uncaughtException') 監(jiān)聽這種情況

    • 代碼執(zhí)行過程中觸發(fā)了未處理的 promise rejection(Node.js v16 開始會導(dǎo)致進(jìn)程退出),可以通過 process.on('unhandledRejection') 監(jiān)聽這種情況

    • EventEmitter 觸發(fā)了未監(jiān)聽的 error 事件

    • 代碼中主動調(diào)用 process.exit 函數(shù)退出進(jìn)程,可以通過 process.on('exit') 監(jiān)聽

    • Node.js 的事件隊列為空,可簡單認(rèn)為沒有需要執(zhí)行的代碼了,可以通過 process.on('exit') 監(jiān)聽

    我們知道 pm2 有守護(hù)進(jìn)程的效果,在你的進(jìn)程發(fā)生錯誤退出時,pm2 會重啟你的進(jìn)程,我們也在 Node.js 的 cluster 模式下,實(shí)現(xiàn)一個守護(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(`啟動主進(jìn)程: ${process.pid}`);
      // 根據(jù) cpu 核數(shù),創(chuàng)建工作進(jìn)程
      for (let i = 0; i < numCPUs; i++) {
        cluster.fork();
      }
      // 監(jiān)聽工作進(jìn)程退出事件
      cluster.on('exit', (worker, code, signal) => {
        console.log(`工作進(jìn)程 ${worker.process.pid} 退出,錯誤碼: ${code || signal}, 重啟中...`);
        // 重啟子進(jìn)程
        cluster.fork();
      });
    }
    
    // 工作進(jìn)程代碼
    if (cluster.isWorker) {
      // 監(jiān)聽未捕獲錯誤事件
      process.on('uncaughtException', error => {
        console.log(`工作進(jìn)程 ${process.pid} 發(fā)生錯誤`, error);
        process.emit('disconnect');
        process.exit(1);
      });
      // 創(chuàng)建 web server
      // 各個工作進(jìn)程都會監(jiān)聽端口 8000(Node.js 內(nèi)部會做處理,不會導(dǎo)致端口沖突)
      http.createServer((req, res) => {
        res.writeHead(200);
        res.end('hello world\n');
      }).listen(8000);
      console.log(`啟動工作進(jìn)程: ${process.pid}`);
    }

    應(yīng)用實(shí)踐

    上面分析了 Node.js 進(jìn)程退出的各種情況,現(xiàn)在我們來做一個監(jiān)聽進(jìn)程退出的工具,在 Node.js 進(jìn)程退出時,允許使用方執(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)聽各種退出事件
    process.on('exit', code => handleExit(code));
    // 按照 POSIX 的規(guī)范,我們用 128 + 信號編號 得到最終的退出碼
    // 信號編號參考下面的圖片,大家可以在 linux 系統(tǒng)下執(zhí)行 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 代表未捕獲的錯誤導(dǎo)致進(jìn)程退出
    process.on('uncaughtException', error => handleExit(1, error));
    process.on('unhandledRejection', error => handleExit(1, error));

    信號編號:

    Node.js進(jìn)程退出的原理是什么

    接下來我們要實(shí)現(xiàn)真正的進(jìn)程退出函數(shù) handleExit,因?yàn)橛脩魝魅氲娜蝿?wù)函數(shù)可能是同步的,也可能是異步的;我們可以借助 process.nextTick 來保證用戶的同步代碼都已經(jīng)執(zhí)行完成,可以簡單理解 process.nextTick 會在每個事件循環(huán)階段的同步代碼執(zhí)行完成后執(zhí)行(理解 process.nextTick);針對異步任務(wù),我們需要用戶調(diào)用 callback 來告訴我們異步任務(wù)已經(jīng)執(zhí)行完成了:

    // 標(biāo)記是否正在退出,避免多次執(zhí)行
    let isExiting = false;
    const handleExit = (code, error) => {
      if (isExiting) return;
      isExiting = true;
    
      // 標(biāo)記已經(jīng)執(zhí)行了退出動作,避免多次調(diào)用
      let hasDoExit = fasle;
      const doExit = () => {
          if (hasDoExit) return;
          hasDoExit = true
          process.nextTick(() => process.exit(code))
      }
    
      // 記錄有多少個異步任務(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ù)個數(shù)大于 1,認(rèn)為傳遞了 callback 參數(shù),是一個異步任務(wù)
          if (taskFn.length > 1) {
             asyncTaskCount++
             taskFn(error, ayncTaskCallback)
          } else {
              taskFn(error)
          }
      });
    
      // 如果存在異步任務(wù)
      if (asyncTaskCount > 0) {
          // 超過 10s 后,強(qiáng)制退出
          setTimeout(() => {
              doExit();
          }, 10 * 1000)
      } else {
          doExit()
      }
    };

    至此,我們的進(jìn)程退出監(jiān)聽工具就完成了,完整的實(shí)現(xiàn)可以查看這個開源庫 async-exit-hook

    進(jìn)程優(yōu)雅退出

    通常我們的 web server 在重啟、被運(yùn)行容器調(diào)度(pm2 或者 docker 等)、出現(xiàn)異常導(dǎo)致進(jìn)程退出時,我們希望執(zhí)行退出動作,如完成已經(jīng)連接到服務(wù)的請求響應(yīng)、清理數(shù)據(jù)庫連接、打印錯誤日志、觸發(fā)告警等,做完退出動作后,再退出進(jìn)程,我們可以使用剛才的進(jìn)程退出監(jiān)聽工具實(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);
    
    // 使用我們在上面開發(fā)的工具添加進(jìn)程退出任務(wù)
    addExitTask((error, callback) => {
       // 打印錯誤日志、觸發(fā)告警、釋放數(shù)據(jù)庫連接等
       console.log('進(jìn)程異常退出', error)
       // 停止接受新的請求
       server.close((error) => {
           if (error) {
             console.log('停止接受新請求錯誤', error)
           } else {
             console.log('已停止接受新的請求')
           }
       })
       // 比較簡單的做法是,等待一定的時間(這里我們等待 5s),讓存量請求執(zhí)行完畢
       // 如果要完全保證所有請求都處理完畢,需要記錄每一個連接,在所有連接都釋放后,才執(zhí)行退出動作
       // 可以參考開源庫 https://github.com/sebhildebrandt/http-graceful-shutdown
       setTimout(callback, 5 * 1000)
    })

    以上就是“Node.js進(jìn)程退出的原理是什么”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注億速云行業(yè)資訊頻道。

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

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI