>  기사  >  웹 프론트엔드  >  Node.js 프로세스가 종료될 수 있는 다양한 상황에 대해 이야기해 보겠습니다.

Node.js 프로세스가 종료될 수 있는 다양한 상황에 대해 이야기해 보겠습니다.

青灯夜游
青灯夜游앞으로
2022-04-02 20:08:423431검색

이 글은 Node의 프로세스 종료에 대해 이야기하고 Node.js 프로세스가 종료될 수 있는 다양한 상황을 소개합니다. 모두에게 도움이 되기를 바랍니다!

Node.js 프로세스가 종료될 수 있는 다양한 상황에 대해 이야기해 보겠습니다.

서비스가 출시된 후에는 운영 환경(예: 컨테이너, pm2 등)에 따라 불가피하게 예약되며, 서비스 업그레이드로 인해 다시 시작되고, 일반적으로 다양한 예외로 인해 프로세스가 중단될 수 있습니다. 운영 환경에는 서비스 프로세스에 대한 제한이 있습니다. 프로세스가 비정상일 때 상태 모니터링은 프로세스를 다시 시작하며 롤링 업그레이드 전략도 있습니다. 그러나 실행 중인 환경의 스케줄링 전략은 우리의 서비스 프로세스를 블랙박스로 취급하고 서비스 프로세스의 내부 실행 조건에는 신경 쓰지 않습니다. 따라서 우리의 서비스 프로세스는 실행 중인 환경의 스케줄링 작업을 적극적으로 감지하고 수행해야 합니다. 일부 종료 정리 작업.

그래서 오늘은 Node.js 프로세스가 종료될 수 있는 다양한 상황과 이러한 프로세스 종료 이벤트를 수신하여 수행할 수 있는 작업을 정리하겠습니다.

원리

프로세스는 두 가지 상황에서 종료해야 합니다. 하나는 프로세스가 자체적으로 종료되는 것이고, 다른 하나는 프로세스 종료를 요구하는 시스템 신호를 수신하는 것입니다.

종료에 대한 시스템 신호 알림

일반적인 시스템 신호는 Node.js 공식 문서에 나열되어 있습니다.

  • SIGHUP: ctrl+c를 통해 프로세스를 중지하지 말고 닫으세요. 명령줄 터미널이 이 신호를 트리거합니다
  • SIGINT: 프로세스를 중지하기 위해 Ctrl+C를 누를 때 트리거됩니다. pm2가 하위 프로세스를 다시 시작하거나 중지하면 이 신호도 하위 프로세스로 보냅니다.
  • SIGTERM: 일반적으로 예를 들어 k8s가 포드를 삭제하면 포드는 시간 초과 기간(기본값 30초) 내에 일부 종료 정리 작업을 수행할 수 있습니다. 윈도우 시스템에서는 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')을 통해 이 상황을 모니터링할 수 있습니다.
  • 코드 실행 중에 처리되지 않은 약속 거부가 발생합니다(Node.js v16으로 인해 프로세스가 종료됩니다). 프로세스를 통해 이 상황을 모니터링할 수 있습니다. .on('unhandledRejection') 상황
  • EventEmitter가 모니터링되지 않는 오류 이벤트를 트리거합니다.
  • 프로세스를 종료하기 위해 코드에서 process.exit 함수가 적극적으로 호출됩니다. process.on('exit')을 통해 이를 모니터링할 수 있습니다. Node.js의 이벤트 큐는 비어 있다고 생각하면 됩니다. 실행해야 할 코드는 process.on('exit')을 통해 모니터링할 수 있습니다
  • pm2에는 데몬 프로세스의 효과가 있다는 것을 알고 있습니다. . 프로세스가 오류로 종료되면 pm2는 프로세스를 다시 시작합니다. 또한 Node.js의
  • 클러스터 모드
  • 에서는 데몬 하위 프로세스의 효과가 달성됩니다.
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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제