专栏首页程序员成长指北简单分析下 Node.js 关于集群的那些事

简单分析下 Node.js 关于集群的那些事

作者:hpstream 文章地址:https://www.yuque.com/docs/share/3bed0240-047e-4a49-a989-f0a37fc28971?# 《简单分析下 Node.js 关于集群的那些事》

前言:

需要了解的基础概念 一个应用程序中,至少包含一个进程,一个进程至少包含一个线程。

  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位
  • 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

Node 的特点:

  • 主线程是单进程(后面版本出现了线程概念,开销较大);
  • 基于事件驱动,异步非阻塞 I/O;
  • 可用于高并发场景。

nodejs 原有版本中没有实现多线程,为了充分利用多核 cpu,可以使用子进程实现内核的负载均衡。

node 需要解决的问题:

  • node 做耗时的计算时候,造成阻塞。
  • node 如何开启子进程
  • 开发过程中如何实现进程守护

概念太多,我们从具体案例入手,看看单线程到底会带来什么问题。

单线程的缺点

// file: question.js
const http = require('http');
http.createServer((req, res) => {
  if (req.url === '/sum') { // 求和
    var endTime = new Date().getTime() + 10000
    while (new Date().getTime() < endTime) {}
    res.end('sum')
  } else {
    res.end('end');
  }
}).listen(3000);

操作步骤

  • node question.js
  • 打开浏览器,在一个 tab1 上访问 /sum 。快速打开另一个 tab2,访问 / 。

请问会出现什么现象? 我们发现 tab1 在转圈, tab2 也在转圈,这个现象就很奇怪了。tab1 在转圈我们可以理解,因为我们需要花费是 10s,但是 tab2 也需要 10s 后,才能被访问。这就很奇怪了。

这个问题就相当于,别人访问这个浏览器阻塞了 10s,你也要跟着阻塞 10s。这个问题就很难被接受了。因此得出结论,node 不太适合做 cpu 密集型的服务。

如何解决这个问题?

为了解决这个问题,我们引入子进程。

file: calc.js

var endTime = new Date().getTime() + 10000
while (new Date().getTime() < endTime) {}

process.send({
    time: new Date().getTime()+''
});

改造 question.js

file: question.js
const http = require('http');
const {fork} = require('child_process');
const path = require('path');
http.createServer((req, res) => {
  if (req.url === '/sum') { // 求和
      // var endTime = new Date().getTime() + 10000
      // while (new Date().getTime() < endTime) {}
      // res.end('sum')
      let childProcess = fork('calc.js', {
        cwd: path.resolve(__dirname)
      });
      childProcess.on('message', function (data) {
        res.end(data.time + '');
      })
  } else {
    res.end('end');
  }
}).listen(3001);

重新启动 node question.js,发现 tab2,就不会阻塞了。

总结:node 作为服务器的话,需要开启子进程来解决 cpu 密集型的操作。以防止主线程被阻塞

子进程的使用 (child_process)

使用的方法

  • spawn 异步生成子进程
  • fork 产生一个新的 Node.js 进程,并使用建立的 IPC 通信通道调用指定的模块,该通道允许在父级和子级之间发送消息。
  • exec 产生一个 shell 并在该 shell 中运行命令
  • execFile 无需产生 shell

spawn

spawn 产卵,可以通过此方法创建一个子进程

let { spawn } = require("child_process");
let path = require("path");
// 通过node命令执行sub_process.js文件
let childProcess = spawn("node",['sub_process.js'], {
  cwd: path.resolve(__dirname, "test"), // 找文件的目录是test目录下
  stdio: [0, 1, 2]
});
// 监控错误
childProcess.on("error", function(err) {
  console.log(err);
});
// 监听关闭事件
childProcess.on("close", function() {
  console.log("close");
});
// 监听退出事件
childProcess.on("exit", function() {
  console.log("exit");
});

stdio 这个属性非常有特色,这里我们给了 0,1,2 那么分别代表什么呢? stdio

  1. 0,1,2 分别对应当前主进程的 process.stdin,process.stdout,process.stderr,意味着主进程和子进程共享标准输入和输出
let childProcess = spawn("node",['sub_process.js'], {
  cwd: path.resolve(__dirname, "test"), // 找文件的目录是test目录下
  stdio: [0, 1, 2]
});

可以在当前进程下打印 sub_process.js 执行结果

  1. 默认不提供 stdio 参数时,默认值为 stdio:['pipe'],也就是只能通过流的方式实现进程之间的通信
let { spawn } = require("child_process");
let path = require("path");
// 通过node命令执行sub_process.js文件
let childProcess = spawn("node",['sub_process.js'], {
  cwd: path.resolve(__dirname, "test"),
  stdio:['pipe'] // 通过流的方式
});
// 子进程读取写入的数据
childProcess.stdout.on('data',function(data){
    console.log(data);
});
// 子进程像标准输出中写入
process.stdout.write('hello');
  1. 使用 ipc 方式通信,设置值为 stdio:['pipe','pipe','pipe','ipc'],可以通过 on('message')和 send 方法进行通信
  let { spawn } = require("child_process");
  let path = require("path");
  // 通过node命令执行sub_process.js文件
  let childProcess = spawn("node",['sub_process.js'], {
    cwd: path.resolve(__dirname, "test"),
    stdio:['pipe','pipe','pipe','ipc'] // 通过流的方式
  });
  // 监听消息
  childProcess.on('message',function(data){
      console.log(data);
  });
  // 发送消息
  process.send('hello');
  1. 还可以传入ignore 进行忽略 , 传入inherit表示默认共享父进程的标准输入和输出

产生独立进程

let { spawn } = require("child_process");
let path = require("path");
// 通过node命令执行sub_process.js文件
let child = spawn('node',['sub_process.js'],{
    cwd:path.resolve(__dirname,'test'),
    stdio: 'ignore',
    detached:true // 独立的线程
});
child.unref(); // 放弃控制

作用:开启线程后,并且放弃对线程的控制。我们就可以不占用控制太后台运行了。

fork

衍生新的进程,默认就可以通过ipc方式进行通信

let { fork } = require("child_process");
let path = require("path");
// 通过node命令执行sub_process.js文件
let childProcess = fork('sub_process.js', {
  cwd: path.resolve(__dirname, "test"),
});
childProcess.on('message',function(data){
    console.log(data);
});

fork是基于spawn的,可以多传入一个silent属性, 设置是否共享输入和输出

fork原理

function fork(filename,options){
    let stdio = ['inherit','inherit','inherit']
    if(options.silent){ // 如果是安静的  就忽略子进程的输入和输出
        stdio = ['ignore','ignore','ignore']
    }
    stdio.push('ipc'); // 默认支持ipc的方式
    options.stdio = stdio
    return spawn('node',[filename],options)
}

execFile

通过node命令,直接执行某个文件

let childProcess = execFile("node",['./test/sub_process'],function(err,stdout,stdin){
    console.log(stdout); 
});

内部调用的是spawn方法

exec

let childProcess = exec("node './test/sub_process'",function(err,stdout,stdin){
    console.log(stdout)
});

内部调用的是execFile,其实以上的三个方法都是基于spawn

实现集群

// file cluster.js 主线程
// 内部原理就是多进程 
// 分布式  前端和后端  集群 多个功能相同的来分担工作
// 集群 就可以实现多个cpu的负载均衡 一般情况 
// 不同进程 监听同一个端口号
const {fork}  = require('child_process');
const cpus = require('os').cpus().length;
const path = require('path');

// 现在主进程中先启动一个服务
const http = require('http');
let server = http.createServer(function (req,res) {
    res.end(process.pid+' '+ ' main end')
}).listen(3000);

for(let i = 0 ; i < cpus-1 ; i++ ){
    let cp = fork('server.js',{cwd:path.resolve(__dirname,'worker'),stdio:[0,1,2,'ipc']});
    cp.send('server',server); // 我可以在ipc 模式下第二个参数传入一个http服务 或者tcp服务
}
// 多个请求都是i/o密集
// cluster 集群
// file  worker/server.js 子进程
const http = require('http');

process.on('message',function (data,server) {
    http.createServer(function (req,res) {
        
        res.end(process.pid+' '+ 'end')
    }).listen(server); // 多进程监控同一个端口号 
})

// file http.get.js 请求脚本
const http = require('http');


for(let i =0 ; i < 10000;i++){
    http.get({
        port:3000,
        hostname:'localhost'
    },function (res) {
        res.on('data',function (data) {
            console.log(data.toString())
        })
    })
}

启动请求脚本以后,多次发送请,可以清楚的发现请求的进程pid 不是同一个pid。

cluster模块实现集群

let cluster = require("cluster");
let http = require("http");
let cpus = require("os").cpus().length;
const workers = {};
if (cluster.isMaster) {
    cluster.on('exit',function(worker){
        console.log(worker.process.pid,'death')
        let w = cluster.fork();
        workers[w.pid] = w;
    })
  for (let i = 0; i < cpus; i++) {
    let worker = cluster.fork();
    workers[worker.pid] = worker;
  }
} else {
  http
    .createServer((req, res) => {
      res.end(process.pid+'','pid');
    })
    .listen(3000);
  console.log("server start",process.pid);
}

上诉的代码有点反人类,但是 c++ 中也是存在这样操作进程的。

另一种方式

// file  

const cluster = require('cluster');
const cpus = require('os').cpus();

// 入口文件

cluster.setupMaster({
    exec: require('path').resolve(__dirname,'worker/cluster.js'),
});

cluster.on('exit',function (worker) {
    console.log(worker.process.pid);
    cluster.fork(); // 在开启个进程
})
for(let i = 0; i < cpus.length ;i++){
    cluster.fork(); // child_process fork  会以当前文件创建子进程
    // 并且isMaster 为false 此时就会执行else方法
}
// pm2 专门 开启 重启 直接采用集群的方式
// 模块
// node worker/cluster.js 
// 我们的项目逻辑很多 
  const http = require('http');
  http.createServer((req, res) => {

    if (Math.random() > 0.5) {
      SDSADADSSA();
    }
    // 在集群的环境下可以监听同一个端口号
    res.end(process.pid + ':' + 'end')
  }).listen(3000);

pm2应用

pm2可以把你的应用部署到服务器所有的CPU上,实现了多进程管理、监控、及负载均衡

安装pm2

npm install pm2 -g # 安装pm2
pm2 start server.js --watch -i max # 启动进程
pm2 list # 显示进程状态
pm2 kill # 杀死全部进程
pm2 start npm -- run dev # 启动npm脚本

pm2配置文件

pm2 ecosystem

配置项目自动部署

module.exports = {
  apps : [{
    name: 'my-project',
    script: 'server.js',
    // Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/
    args: 'one two',
    instances: 2,
    autorestart: true,
    watch: false,
    max_memory_restart: '1G',
    env: {
      NODE_ENV: 'development'
    },
    env_production: {
      NODE_ENV: 'production'
    }
  }],
  deploy : {
    production : {
      user : 'root',
      host : '39.106.14.146',
      ref  : 'origin/master',
      repo : 'https://github.com/wakeupmypig/pm2-deploy.git',
      path : '/home',
      'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production'
    }
  }
};
pm2 deploy ecosystem.config.js production setup # 执行git clone
pm2 deploy ecosystem.config.js production # 启动pm2

本文分享自微信公众号 - 程序员成长指北(coder_growth),作者:hpstream

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-11-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 作为一个前端工程师也要掌握的几种文件路径知识

    之前在做webpack配置时候多次用到路径相关内容。最近在写项目的时候,有一个文件需要上传到阿里云oss的功能,同时本地服务器也需要保留一个文件备份。多次用到了...

    coder_koala
  • 自己实现一个简易的模块打包器(干货)

    作者:海因斯坦,原文链接:https://juejin.im/post/6893809205183479822

    coder_koala
  • SVG实现环形进度条的原理

    之前在项目中遇到一个环形进度条的需求,要求能实时更新进度,脑海中瞬间便蹦出css,svg,canvas3中方案,对于3种方案个人更偏向于svg,用法简单,代码量...

    coder_koala
  • 【zookeeper】安装指南

    http://www.apache.org/dyn/closer.cgi/zookeeper/

    王亚昌
  • 数据去重算法(一)

    在编写代码时,经常会遇到对一组数据过滤去除重复的数据,那么怎么来实现这样的一个功能函数呢?

    暮雨
  • Mac下的Jenkins安装

    1)通过命令行安装   brew install jenkins,可能会遇到先更新 brew 的情况  https://brew.sh/index_zh-cn;

    meteoric
  • 【图解数据结构】 栈&队列

    撸码那些事
  • 使用Faster-Rcnn进行目标检测(实践篇)

    原理 上一篇文章,已经说过了,大家可以参考一下,Faster-Rcnn进行目标检测(原理篇) 实验 我使用的代码是python版本的Faster Rcnn,官方...

    GavinZhou
  • 设计模式-UML关系基础

    mySoul
  • A轮融资380万美元,印度VR家装创企Foyr有望明年推出新产品

    VRPinea

扫码关注云+社区

领取腾讯云代金券