ホームページ  >  記事  >  ウェブフロントエンド  >  Node.js プロセスの終了を引き起こす可能性のあるさまざまな状況について話しましょう

Node.js プロセスの終了を引き起こす可能性のあるさまざまな状況について話しましょう

青灯夜游
青灯夜游転載
2022-04-02 20:08:423557ブラウズ

この記事では、Node のプロセス終了について説明し、Node.js プロセスの終了を引き起こす可能性のあるさまざまな状況を紹介します。

Node.js プロセスの終了を引き起こす可能性のあるさまざまな状況について話しましょう

サービスのリリース後は、動作環境(コンテナ、pm2など)によって必然的にスケジュールが設定され、サービスのアップグレードにより再起動が発生したり、さまざまな例外が発生したりします。プロセスがクラッシュする可能性があります; 一般的に次に、オペレーティング環境にはサービス プロセスの健全性監視があり、プロセスが異常な場合はプロセスが再起動されます。アップグレード時には、ローリング アップグレード戦略もあります。ただし、実行環境のスケジューリング戦略はサービス プロセスをブラック ボックスとして扱い、サービス プロセスの内部実行状態を考慮しないため、サービス プロセスは実行環境のスケジューリング アクションを積極的に感知して実行する必要があります。いくつかの終了クリーンアップ アクション。

そこで、今日は、Node.js プロセスの終了を引き起こす可能性のあるさまざまな状況と、これらのプロセス終了イベントをリッスンすることで何ができるかを整理します。

原則

プロセスが終了したい場合、状況は 2 つだけあります。1 つはプロセスが積極的に終了すること、もう 1 つはプロセスが終了することです。システム信号を受信したことを通知し、プロセスに終了を要求します。

終了するためのシステム シグナル通知

一般的なシステム シグナルは、Node.js 公式ドキュメント にリストされています。主に次のいくつかに焦点を当てます。

    ##SIGHUP: Ctrl C を押してプロセスを停止するのではなく、コマンド ライン ターミナルを直接閉じると、このシグナルがトリガーされます。
  • ##SIGINT: Ctrl C を押してプロセスを停止するとトリガーされます。 ; pm2 が再起動または停止する 子プロセスが実行中の場合、シグナルは子プロセスにも送信されます
  • SIGTERM: 通常、プロセスに正常に終了するように通知するために使用されます。たとえば、k8s がポッドを削除すると、 SIGTERM シグナルをポッドに送信します。ポッドはタイムアウト期間内である可能性があります (デフォルトは 30 秒)。いくつかの終了クリーニング アクションを実行します。
  • SIGBREAK: ウィンドウ システムでは、Ctrl Break を押すとこのシグナルがトリガーされます
  • SIGKILL: プロセスを強制的に終了します。プロセスはクリーニング アクションを実行できません。コマンド kill -9 pid を実行すると、プロセスはこのシグナルを受け取ります。 k8s がポッドを削除するときに、30 秒以上かかり、ポッドが終了していない場合、k8s は SIGKILL シグナルをポッドに送信し、すぐにポッド プロセスを終了します。pm2 がプロセスを再起動または停止するとき、1.6 秒以上かかった場合
  • 非強制終了シグナルを受信すると、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 プロセスの終了を引き起こす可能性のあるさまざまな状況について話しましょう#プロセスはアクティブに終了します

#Node.js プロセスは主に次のような状況でアクティブに終了します:

コードの実行中にキャッチされないエラーがトリガーされます。この状況は process.on('uncaughtException')

を通じて監視できます。

コード実行中にトリガーされる未処理の Promise 拒否 (Node.js v16 ではプロセスが終了します)。process.on('unhandledRejection') を通じてこの状況を監視できます
  • EventEmitter は監視されていないエラー イベントをトリガーします
  • コードでは、プロセスを終了するために process.exit 関数が積極的に呼び出されています。process.on('exit') を通じて監視できます。
  • Node.js のイベント キューは次のとおりです。空です。実行する必要のあるコードがないと単純に考えることができます。process.on('exit') listen を使用できます。
  • pm2 にはデーモン プロセスの効果があることがわかっています。プロセスがエラーで終了すると、pm2 はプロセスを再起動します。Node.js も使用しています。
  • クラスター モード
  • は、デーモン サブプロセスの効果を実現します (実際、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(&#39;exit&#39;, (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 退出,错误码: ${code || signal}, 重启中...`);
    // 重启子进程
    cluster.fork();
  });
}

// 工作进程代码
if (cluster.isWorker) {
  // 监听未捕获错误事件
  process.on(&#39;uncaughtException&#39;, error => {
    console.log(`工作进程 ${process.pid} 发生错误`, error);
    process.emit(&#39;disconnect&#39;);
    process.exit(1);
  });
  // 创建 web server
  // 各个工作进程都会监听端口 8000(Node.js 内部会做处理,不会导致端口冲突)
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(&#39;hello world\n&#39;);
  }).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(&#39;exit&#39;, code => handleExit(code));
// 按照 POSIX 的规范,我们用 128 + 信号编号 得到最终的退出码
// 信号编号参考下面的图片,大家可以在 linux 系统下执行 kill -l 查看所有的信号编号
process.on(&#39;SIGHUP&#39;, () => handleExit(128 + 1));
process.on(&#39;SIGINT&#39;, () => handleExit(128 + 2));
process.on(&#39;SIGTERM&#39;, () => handleExit(128 + 15));
// windows 下按下 ctrl+break 的退出信号
process.on(&#39;SIGBREAK&#39;, () => handleExit(128 + 21));
// 退出码 1 代表未捕获的错误导致进程退出
process.on(&#39;uncaughtException&#39;, error => handleExit(1, error));
process.on(&#39;unhandledRejection&#39;, 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(&#39;http&#39;);

// 创建 web server
const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end(&#39;hello world\n&#39;);
}).listen(8000);

// 使用我们在上面开发的工具添加进程退出任务
addExitTask((error, callback) => {
   // 打印错误日志、触发告警、释放数据库连接等
   console.log(&#39;进程异常退出&#39;, error)
   // 停止接受新的请求
   server.close((error) => {
       if (error) {
         console.log(&#39;停止接受新请求错误&#39;, error)
       } else {
         console.log(&#39;已停止接受新的请求&#39;)
       }
   })
   // 比较简单的做法是,等待一定的时间(这里我们等待 5s),让存量请求执行完毕
   // 如果要完全保证所有请求都处理完毕,需要记录每一个连接,在所有连接都释放后,才执行退出动作
   // 可以参考开源库 https://github.com/sebhildebrandt/http-graceful-shutdown
   setTimout(callback, 5 * 1000)
})

总结

通过上面的文字,相信你已经对导致 Node.js 进程退出的各种情况心里有数了。在服务上线后,虽然 k8s、pm2 等工具能够在进程异常退出时,不停地拉起进程,保证服务的可用性,但我们也应该在代码中主动感知进程的异常或者被调度的情况,从而能够更早发现问题。

更多node相关知识,请访问:nodejs 教程

以上がNode.js プロセスの終了を引き起こす可能性のあるさまざまな状況について話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。