17

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

 3 years ago
source link: https://mp.weixin.qq.com/s/v_BQviy2cUcYpG_8x0WCGQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者: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

:heart:爱心三连击

1.看到这里了就点个在看支持下吧,你的 点赞 在看 是我创作的动力。

2.关注公众号 程序员成长指北 ,回复「1」加入Node进阶交流群!「在这里有好多 Node 开发者,会讨论 Node 知识,互相学习」!

3.也可添加微信【 ikoala520 】,一起成长。

eYjQrer.jpg!mobile

“在看转发” 是最大的支持


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK