Home >Web Front-end >JS Tutorial >Let's talk about various situations that may cause the Node.js process to exit

Let's talk about various situations that may cause the Node.js process to exit

青灯夜游
青灯夜游forward
2022-04-02 20:08:423633browse

This article talks about the process exit of Node and introduces various situations that may cause the Node.js process to exit. I hope it will be helpful to everyone!

Let's talk about various situations that may cause the Node.js process to exit

After our service is released, it will inevitably be scheduled by the operating environment (such as containers, pm2, etc.), service upgrades will cause restarts, and various exceptions will cause the process to crash; in general Next, the operating environment has health monitoring of the service process. When the process is abnormal, the process will be restarted. When upgrading, there is also a rolling upgrade strategy. However, the scheduling strategy of the running environment treats our service process as a black box and does not care about the internal running conditions of the service process. Therefore, our service process needs to actively sense the scheduling actions of the running environment and then perform some exit cleanup actions. .

So today we will sort out various situations that may cause the Node.js process to exit, and what we can do by listening to these process exit events.

Principle

If a process wants to exit, there are only two situations. One is that the process actively exits, and the other is that it receives a system signal. Ask the process to exit.

System signal notification to exit

Common system signals are listed in the Node.js official document. We mainly focus on a few:

  • SIGHUP: Instead of stopping the process through ctrl c, but directly closing the command line terminal, this signal will be triggered
  • SIGINT: Triggered when ctrl c is pressed to stop the process; pm2 restarts or stops When a child process is running, the signal will also be sent to the child process
  • SIGTERM: Generally used to notify the process to exit gracefully. For example, when k8s deletes a pod, it will send the SIGTERM signal to the pod. The pod can be within the timeout period (default 30s) Do some exit cleaning actions
  • SIGBREAK: On the window system, pressing ctrl break will trigger this signal
  • SIGKILL: Force to exit the process, the process cannot perform any cleaning actions, execute the command kill -9 pid, the process will receive this signal. When k8s deletes a pod, if it takes more than 30 seconds and the pod has not exited, k8s will send a SIGKILL signal to the pod and immediately exit the pod process; when pm2 restarts or stops the process, if it takes more than 1.6 seconds and the process has not exited, it will also send a SIGKILL signal.

When receiving a non-forced exit signal, the Node.js process can listen to the exit signal and do some customized exit logic. For example, we wrote a cli tool that takes a long time to execute a task. If the user wants to exit the process through ctrl c before the task is completed, the user can be prompted to wait:

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();

The implementation effect is as follows, Every time ctrl c is pressed, the user will be prompted:

Lets talk about various situations that may cause the Node.js process to exit

The process actively exits

The Node.js process actively exits, mainly including the following Several situations:

  • Uncaught errors are triggered during code execution. You can monitor this situation through process.on('uncaughtException')
  • Triggered during code execution Unhandled promise rejection (Node.js v16 will cause the process to exit), you can monitor this situation through process.on('unhandledRejection')
  • EventEmitter triggers the unmonitored error event
  • In the code, the process.exit function is actively called to exit the process. You can monitor it through process.on('exit').
  • The event queue of Node.js is empty. You can simply think that there is no code that needs to be executed. You can use process.on('exit') listening

We know that pm2 has the effect of a daemon process. When your process exits with an error, pm2 will restart your process. We are also using Node.js cluster mode realizes the effect of a daemon sub-process (actually pm2 has similar logic):

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}`);
}

Application practice

The various situations in which the Node.js process exits have been analyzed above. Now we will make a tool to monitor the process exit. When the Node.js process exits, the user is allowed to execute its own exit logic:

// 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));

Signal number:

Lets talk about various situations that may cause the Node.js process to exit

接下来我们要实现真正的进程退出函数 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 教程

The above is the detailed content of Let's talk about various situations that may cause the Node.js process to exit. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete