NodeJS Cluster模块基础教程

Cluster简介

默认情况下,Node.js不会利用所有的CPU,即使机器有多个CPU。一旦这个进程崩掉,那么整个 web 服务就崩掉了。

应用部署到多核服务器时,为了充分利用多核 CPU 资源一般启动多个 NodeJS 进程提供服务,这时就会使用到 NodeJS 内置的 Cluster 模块了。Cluster模块可以创建同时运行的子进程(Worker进程),同时共享同一个端口。每个子进程都有自己的事件循环内存V8实例

NodeJS Cluster是基于Master-Worker模型的,Master负责监控Worker的状态并分配工作任务,Worker则负责执行具体的任务。Master和Worker之间通过IPC(进程间通信)传递消息,进程之间没有共享内存

主进程也做叫Master进程,子进程也叫做Worker进程,下面会混用这两种叫法

HTTP服务器和Cluster

使用NodeJS构建http服务器非常简单,代码如下:

//app.js
const http = require("http");
const pid = process.pid;
http
  .createServer((req, res) => {
    for (let i = 1e7; i > 0; i--) {}
    console.log(`Handling request from ${pid}`);
    res.end(`Hello from ${pid}\n`);
  })
  .listen(8081, () => {
    console.log(`Started ${pid}`);
  });
****

为了模拟一些实际的CPU工作,我们执行了1000万次空循环,启动服务之后,可以使用浏览
器或curl向http://localhost:8080发送请求

curl localhost:8081

返回如下:

Hello from 33720

使用autocannon压测服务器

安装autocannon:

npm i -g autocannon

使用autocannon:

autocannon -c 200 -d 10 http://localhost:8081

上面的命令将在10秒内为服务器发起200个并发连接

运行结果如下:

┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬───────────┬─────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%   │ 99%     │ Avg       │ Stdev     │ Max     │
├─────────┼────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Latency │ 453 ms │ 651 ms │ 1003 ms │ 1829 ms │ 750.95 ms │ 208.08 ms │ 1968 ms │
└─────────┴────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘

3k requests in 10.02s

Latency表示延迟,可以看到平均延迟是750ms,最慢的响应延迟接近2S,在10S服务器一共接受了3000请求

使用Cluster模块进行扩展

const cluster = require("node:cluster");
const http = require("node:http");
const numCPUs = require("node:os").cpus().length;
const process = require("node:process");
if (cluster.isMaster) {
  //处理主进程逻辑
  masterProcess();
} else {
  //处理子进程逻辑
  childProcess();
}
function masterProcess() {
  console.log(`Master ${process.pid} is running`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
}
function childProcess() {
  http
    .createServer((req, res) => {
      for (let i = 1e7; i > 0; i--) {}
      console.log(`Handling request from ${pid}`);
      res.end(`Hello from ${pid}\n`);
    })
    .listen(8081, () => {
      console.log(`Started ${pid}`);
    });
}

将代码保存在 app.js 文件中并运行执行: $node app.js 。输出类似于下面这样:

Master 33931 is running
Worker Started 33932
Worker Started 33935
Worker Started 33936
Worker Started 33934
Worker Started 33939
Worker Started 33933
Worker Started 33937
Worker Started 33938

通过 isMaster 属性,可以判断是否为 Master 进程,Master进程中执行 cluster.fork()创建与CPU核心数相同的子进程。

fork() 是创建一个新的NodeJS进程,就像通过命令行使用 $node app.js 运行一样,会有很多进程运行 app.js 程序。

子进程创建和执行时,和master一样,导入cluster模块,执行 if 语句。但子进程的 cluster.isMaster的值为 false

fork的过程如下:

在这里插入图片描述

压测Cluster

autocannon -c 200 -d 10 [http://localhost:8081](http://localhost:8081/)

输出结果如下:

┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev    │ Max    │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
│ Latency │ 109 ms │ 136 ms │ 260 ms │ 282 ms │ 142.75 ms │ 32.09 ms │ 397 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘

14k requests in 10.02s

可以看到平均延迟为142ms,最大的延迟为397ms,服务器在10S内一共处理了14000个请求

使用Cluster之后性能提升大约4倍( 14000次/10s 对比 3000次/10s

测试Cluster模块的可用性

为了测试服务的可用性,我们会在子进程中使用setTimeout抛出一些错误

masterProcess方法和childProcess进行修改:

function masterProcess() {
  //...
  //监听子进程的退出事件
  cluster.on("exit", (worker, code) => {
    //子进程异常退出
    if (code !== 0 && !worker.exitedAfterDisconnect) {
      console.log(
        `Worker ${worker.process.pid} crashed. ` + "Starting a new worker"
      );
      cluster.fork();
    }
  });
}
function childProcess() {
  // 随机的1到3秒内等待一段时间,然后抛出一个名为"Ooops"的错误
  setTimeout(() => {
    throw new Error("Ooops");
  }, Math.ceil(Math.random() * 3) * 1000);
  //...
}

在这段代码中,一旦主进程接收到“退出”事件。我们检查code状态码和
worker.exitedAfterDisconnect标记,来判断进程是否为异常退出,然后启动一个新的Worker进程。当终止的Worker进程重新启动时,其他工作进程仍然可以服务请求,从而不会影响应用程序的可用性。

code是用于检查进程的退出码,code为 0,则表示正常退出,如果不是,则表示进程非正常退出。

worker.exitedAfterDisconnect是NodeJS中cluster模块Worker对象的一个属性,用于指示工作进程是否在主进程调用其disconnect()方法后退出。

如果Worker进程成功地完成了disconnect过程并正常退出,则worker.exitedAfterDisconnect将被设置为true。否则,该属性将保持为false,表示该进程已经以其他方式退出。

使用autocannon进行压测autocannon -c 200 -d 10 [http://localhost:8081](http://localhost:8081/)

结果如下:

┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬───────────┬─────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%   │ 99%     │ Avg       │ Stdev     │ Max     │
├─────────┼────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Latency │ 101 ms │ 398 ms │ 1301 ms │ 1498 ms │ 466.99 ms │ 301.07 ms │ 2405 ms │
└─────────┴────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘

14k requests in 10.03s
1k errors (0 timeouts)

14000个请求中,有1000个出现错误,服务的可用性大约为92%,对于一个经常崩溃的应用程序来说,它的可用性也不差

主进程和子进程通信

稍微更新一下之前的代码,就能允许Master进程向Worker进程发送和接收消息,Wordker进程也可以从Master进程接收和发送消息:

function childProcess() {
  console.log(`Worker ${process.pid} started`);
  //监听主进程的消息
  process.on("message", function (message) {
    console.log(
      `Worker ${process.pid} recevies message '${JSON.stringify(message)}'`
    );
  });
  console.log(`Worker ${process.pid} sends message to master...`);
  //给主进程发消息
  process.send({ msg: `Message from worker ${process.pid}` });
}

在子进程中,使用 process.on('message', handler)方法注册一个监听器,当主进程给这个子进程发送消息的时候,会执行handler回调,然后使用 process.send()向主进程发送消息

function masterProcess() {
  console.log(`Master ${process.pid} is running`);
  let workers = [];
  // fork 子进程
  for (let i = 0; i < numCPUs; i++) {
    const worker = cluster.fork();
    workers.push(worker);

    // 监听子进程的消息
    worker.on("message", function (message) {
      console.log(
        `Master ${process.pid} recevies message '${JSON.stringify(
          message
        )}' from worker ${worker.process.pid}`
      );
    });
  }

  // 给每个子进程发送消息
  workers.forEach(function (worker) {
    console.log(
      `Master ${process.pid} sends message to worker ${worker.process.pid}...`
    );
    worker.send({ msg: `Message from master ${process.pid}` });
  }, this);

}

我们先监听子进程的message事件,最后在Master进程给每个 Worker进程发送消息

输出会类似于下面这样:

Master 88498 is running
Master 88498 sends message to worker 88500...
Master 88498 sends message to worker 88501...
Worker 88501 started
Worker 88501 sends message to master...
Master 88498 recevies message '{"msg":"Message from worker 88501"}' from worker 88501
Worker 88501 recevies message '{"msg":"Message from master 88498"}'
Worker 88500 started
Worker 88500 sends message to master...
Master 88498 recevies message '{"msg":"Message from worker 88500"}' from worker 88500
Worker 88500 recevies message '{"msg":"Message from master 88498"}'

使用Cluster进行优雅的重启

当我们更新代码的时候,可能需要重新启动NodeJS。重新启动应用程序时,会出现一个小的空窗期:在我们重启单进程的NodeJS过程中,服务器会无法处理用户的请求

使用Cluster可以解决这个问题,具体做法如下:一次重新启动一个Worker,剩下的Worker可以继续运行处理用户的请求。

在上面代码的基础上对masterProcesschildProcess进行修改:

function masterProcess() {
  console.log(`Master ${process.pid} is running`);
  let workers = [];
  // fork 子进程
  for (let i = 0; i < numCPUs; i++) {
    const worker = cluster.fork();
    workers.push(worker);
  }
  process.on("SIGUSR2", async () => {
    restartWorker(0);
    function restartWorker(i) {
      if (i >= workers.length) return;
      const worker = workers[i];
      console.log(`Stopping worker: ${worker.process.pid}`);

      worker.disconnect(); 
			//监听子进程的退出事件
      worker.on("exit", () => {
				//判断子进程是否完成disconnect过程并正常退出
        if (!worker.exitedAfterDisconnect) return;
        const newWorker = cluster.fork(); //[4]
        newWorker.on("listening", () => {
          //当新的子进程开始监听端口
          //重启下一个子进程
          restartWorker(i + 1);
        });
      });
    }
  });
}
function childProcess() {
  http.createServer((req, res) => {
      console.log("Worker :>> ", `Worker ${process.pid}`);
      res.writeHead(200);
      res.end("hello world\n");
    })
    .listen(8000);
  console.log(`Worker ${process.pid} started`);
}

masterProcess方法新增了process.on("SIGUSR2", callback), SIGUSR2是一种信号,通常用于向一个进程发送自定义的指令,比如要求应用程序执行某些操作(如重启、重新加载配置文件等)。

当主进程接收到SIGUSR2信号时,它会遍历所有Workder进程并调用disconnect方法,然后监听子进程的退出事件。

Workder进程退出之后,Master进程就会重新创建新的Workder进程,并等待其开始监听端口。然后重启下一个Workder进程。

childProcess 方法则是启动了一个Http服务器。

通过这种方式,整个应用程序可以在不中断服务的情况下进行平滑重启,从而实现无缝升级和维护。

在生产环境中,我们一般会使用PM2来进行进程管理,,PM2基于cluster,提供负载均衡、过程监控、零停机重启和其他功能。

总结

本文介绍了使用NodeJS Cluster模块进行多进程处理的方法,包括如何创建子进程、压测Cluster、主进程和子进程通信、以及如何使用Cluster进行优雅的重启。

在生产环境中,我们一般会使用PM2来进行进程管理,PM2基于cluster,提供负载均衡、过程监控、零停机重启和其他功能。下篇文章会介绍一下PM2,敬请期待吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/10309.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

当ChatGPT续写《红楼梦》,能替代原著吗?

来源: 清华大学出版社 近段时间&#xff0c;人工智能聊天机器人ChatGPT火爆网络&#xff0c;“AI写作是否会让文字工作者被替代&#xff1f;”成为人们关注并持续讨论的话题。 闲聊、问答、解题、写代码、写诗、创作小说&#xff0c; 连续回答&#xff0c;不断纠错&#xff0c…

拥抱自动化测试,快速升职加薪丄Selenium+Pytest自动化测试框架教你如何做到

目录&#xff1a;导读 引言 SeleniumPytest自动化测试框架是目前最流行的自动化测试工具之一&#xff0c;其强大的功能和易用性援助许多开发人员和测试人员。 selenium自动化 pytest测试框架禅道实战 选用的测试网址为我电脑本地搭建的禅道 conftest.py更改 config.ini更…

MyBatis配置文件 —— 相关标签详解

目录 一、Mybatis配置文件 — properties标签 二、Mybatis配置文件 — settings标签 三、Mybatis配置文件 — plugins标签 四、Mybatis配置文件 — typeAliases标签 五、Mybatis配置文件 — environments标签 六、Mybatis配置文件 — mappers标签 一、Mybatis配置文件 —…

2023年第十四届蓝桥杯 C++ B组参赛经验总结

没错&#xff0c;今年本菜狗又来啦~~ hhh &#xff0c; 文章当时比赛完就写完了&#xff0c; 发的有点晚 比赛成绩 &#xff08;等出来我就写这里&#xff09; 感觉最多省二 估计没省一了555 赛前准备 赛前把蓝桥杯课基本都刷了 &#xff0c; 但是还是感觉有点慌 刷题经验 …

【网络原理】网络通信与协议

✨个人主页&#xff1a;bit me&#x1f447; ✨当前专栏&#xff1a;Java EE初阶&#x1f447; 目 录一. 网络发展史二. 网络通信基础1. IP地址2. 端口号3. 认识协议&#xff08;核心概念&#xff09;4. 五元组5. 协议分层6. 封装和分用一. 网络发展史 独立模式&#xff1a;计…

springboot从2.1.3升级到2.3.5后控制台自动输出http请求日志RequestResponseBodyMethodProcessor

springboot从2.1.3升级到2.3.5后控制台自动输出http请求日志RequestResponseBodyMethodProcessor和RequestMappingHandlerMapping推荐使用第二个方案简单 明了 方便 快捷方案一第一步定义TurboFilter第二步配置logback方案二 直接配置logback的配置XML推荐使用第二个方案简单 明…

【三十天精通 Vue 3】 第四天 Vue 3的模板语法详解

✅创作者&#xff1a;陈书予 &#x1f389;个人主页&#xff1a;陈书予的个人主页 &#x1f341;陈书予的个人社区&#xff0c;欢迎你的加入: 陈书予的社区 &#x1f31f;专栏地址: 三十天精通 Vue 3 文章目录引言一、Vue 3 模板语法概述1. Vue 3 模板语法的简介2. Vue 3 模板…

Openlayers(五)点位聚合Cluster

Openlayers&#xff08;五&#xff09;点位聚合Cluster 1.业务问题 由于点位在地图上显示过多&#xff0c;会造成页面卡顿、点位标注信息相互叠加导致看不清 优化后效果 不断放大层级 2.聚合类Cluster OpenLayers 中聚合是通过 ol.source.Cluster 实现&#xff0c;聚合的原…

Flink的窗口机制

窗口机制 tumble&#xff08;滚动窗口&#xff09; hop&#xff08;滑动窗口&#xff09; session&#xff08;会话窗口&#xff09; cumulate&#xff08;渐进式窗口&#xff09; Over&#xff08;聚合窗口&#xff09; 滚动窗口&#xff08;tumble&#xff09; 概念 滚…

系统复杂度之【高性能】

系统复杂度之【高性能】 今天我们来谈一谈系统复杂度的根源之【高性能】 对性能的不懈追求一直是人类科技持续发展的核心动力。例如计算机&#xff0c;从电子管计算机到晶体管计算机&#xff0c;再到集成电路计算机&#xff0c;运算性能从每秒几次提高到每秒几亿次。然而&#…

VUE_学习笔记

一、 xx 二、模板语法 1.模板语法之差值语法 &#xff1a;{{ }} 主要研究&#xff1a;{{ 这里可以写什么}} 在data中声明的变量、函数等都可以。常量只要是合法的javascript表达式&#xff0c;都可以。模板表达式都被放在沙盒中&#xff0c;只能访问全局变量的一个白名单&a…

【微服务笔记14】微服务组件之Config配置中心高可用环境搭建

这篇文章&#xff0c;主要介绍微服务组件之Config配置中心高可用环境搭建。 目录 一、高可用Config配置中心 1.1、高可用配置中心介绍 1.2、搭建Eureka注册中心 1.3、搭建ConfigServer服务端 &#xff08;1&#xff09;引入依赖 &#xff08;2&#xff09;添加配置文件 …

Jetson nano部署剪枝YOLOv8

目录前言一、YOLOv8模型剪枝训练1. Pretrain[option]1.1 项目的克隆1.2 数据集1.3 训练2. Constraint training3. Prune4. finetune二、YOLOv8模型剪枝部署1. 源码下载2. 环境配置2.1 trtexec环境变量设置3. ONNX导出3.1 Transpose节点的添加3.2 Resize节点解析的问题4. 运行4.…

FIFO的工作原理及其设计

1.简介 FIFO( First Input First Output)简单说就是指先进先出。FIFO存储器是一个先入先出的双口缓冲器&#xff0c;即第一个进入其内的数据第一个被移出&#xff0c;其中一个口是存储器的输入口&#xff0c;另一个口是存储器的输出口。 对于单片FIFO来说&#xff0c;主要有两种…

SHELL函数可课后作业

一、题目 1、编写函数&#xff0c;实现打印绿色OK和红色FAILED 判断是否有参数&#xff0c;存在为Ok&#xff0c;不存在为FAILED 2、编写函数&#xff0c;实现判断是否无位置参数&#xff0c;如无参数&#xff0c;提示错误 3、编写函数实现两个数字做为参数&#xff0c;返回最…

多线程 之 CAS与synchronized的优化过程

前言 本篇介绍什么是CAS与synchronized的优化过程&#xff0c;如有错误&#xff0c;请在评论区指正&#xff0c;让我们一起交流&#xff0c;共同进步&#xff01; 文章目录前言1. 什么是CAS&#xff1f;2. CAS实现的操作2.1 实现原子类2.2 实现自旋锁3. CAS的aba问题4. synchr…

【无人机】基于灰狼优化算法的无人机路径规划问题研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

链式二叉树及相关操作(前,中,后,层序遍历)

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; “春来无事&#xff0c;只为花忙。” 前言: 上一期给大家介绍了二叉树的一种顺序结构&#xff1a;堆&#xff0c;这一期承接上一期&#xff0c;给大家继续介绍二叉树的另一种结构&#xff1a;链式结构。 目录…

golang指针相关

指针相关的部分实在是没有搞太明白&#xff0c;抽时间来总结下。 1.指针相关基础知识 比如现在有一句话&#xff1a;『谜底666』&#xff0c;这句话在程序中一启动&#xff0c;就要加载到内存中&#xff0c;假如内存地址0x123456&#xff0c;然后我们可以将这句话复制给变量A&…

多线程(八):常见锁策略

目录 前言 1. 乐观锁 VS 悲观锁 乐观锁 悲观锁 2. 轻量级锁 VS 重量级锁 轻量级锁 3. 自旋锁 VS 挂起等待锁 自旋锁 挂起等待锁 4. 读写锁 VS 互斥锁 5. 可重入锁 vs 不可重入锁 死锁 发生死锁的情况 死锁产生的四个必要条件如下&#xff1a; 6. 公平锁和非公平锁…