为什么nodejs是单进程的_浅谈NodeJS多进程服务架构基本原理

如上创建了4个worker进程后,现在我们需要考虑的是如何实现 master进程与worker进程通信的问题。

在NodeJS中父子进程之间通信可以通过 on('message') 和 send()方法来实现通信,on('message') 是监听message事件的。

当该进程收到其他进程发送的消息时候,便会触发message事件。send()方法则是用于向其他进程发送消息的。

具体如何做呢?

master进程中可以调用 child_process的fork()方法后会得到一个子进程的实列,通过该实列我们可以监听到来自子进程的消息或向子进程发送消息。而worker进程则通过process对象接口来监听父进程的消息或向父进程发送消息。现在我们把master.js 代码改成如下:

const childProcess = require('child_process');

const worker= childProcess.fork('./worker.js');//主进程向子进程发送消息

worker.send('Hello World');//监听子进程发送过来的消息

worker.on('message', (msg) =>{

console.log('Received message from worker:' +msg);

});

worker.js 代码如下:

//接收主进程发来的消息

process.on('message', (msg) =>{

console.log('Received message from master:' +msg);//子进程向主进程发送消息

process.send('Hi master.');

});

我们继续在命令中执行 node master.js 命令后,看到如下信息被打印了

3.2 Master实现对Worker的请求进行分发

如上只是简单的父进程和子进程进行通信的demo实列,现在我们继续来看一个更复杂一点的demo。我们知道master进程最主要是创建子进程,及对子进程进行管理和分配,而子进程最主要做的事情是处理具体的请求及业务。

进程通信除了使用到上面的send()方法,发送一些普通对象以外,我们还可以发送句柄,什么是句柄呢,句柄是一种引用,可以用来标识资源。

比如通过句柄可以标识一个socket对象等。我们可以利用该句柄实现请求的分发。

现在我们通过master进程来创建一个TCP服务器来监听一些特定的端口,master进程会收到客户端的请求,我们会得到一个socket对象,通过这个socket对象就可以和客户端进行通信,从而我们可以处理客户端的请求。

比如如下demo实列,master创建TCP服务器并且监听8989端口,收到该请求后会将请求分发给worker处理,worker收到master发来的socket以后,通过socket对客户端的响应。

|------项目| |---master.js| |---worker.js| |---tcp_client.js| |---package.json| |--- node_modules

master.js 代码如下:

const childProcess = require('child_process');

const net= require('net');//获取cpu的数量

const cpuNum = require('os').cpus().length;

let workers=[];

let cur= 0;for (let i = 0; i < cpuNum; ++i) {

workers.push(childProcess.fork('./worker.js'));

console.log('worker process-' +workers[i].pid);

}//创建TCP服务器

const tcpServer =net.createServer();/*服务器收到请求后分发给工作进程去处理*/tcpServer.on('connection', (socket) =>{

workers[cur].send('socket', socket);

cur= Number.parseInt((cur + 1) %cpuNum);

});

tcpServer.listen(8989, () =>{

console.log('Tcp Server: 127.0.0.8989');

});

worker.js 代码如下:

//接收主进程发来的消息

process.on('message', (msg, socket) =>{if (msg === 'socket' &&socket) {//利用setTimeout 模拟异步请求

setTimeout(() =>{

socket.end('Request handled by worker-' +process.pid);

},100);

}

});

tcp.client.js 代码如下:

const net = require('net');

const maxConnectCount= 10;for (let i = 0; i < maxConnectCount; ++i) {

net.createConnection({

port:8989,

host:'127.0.0.1'}).on('data', (d) =>{

console.log(d.toString());

})

}

如上代码,tcp_client.js 负责创建10个本地请求,master.js 首先根据cpu的数量,创建多个worker进程,然后创建一个tcp服务器,使用connection来监听net中 createConnection 方法创建事件,当有事件来的时候,就使用worker子进程依次进行分发事件,最后我们通过worker.js 来使用 process中message事件对事件进行监听。如果收到消息的话,就打印消息出来,比如如下代码:

//接收主进程发来的消息

process.on('message', (msg, socket) =>{if (msg === 'socket' &&socket) {//利用setTimeout 模拟异步请求

setTimeout(() =>{

socket.end('Request handled by worker-' +process.pid);

},100);

}

});

为了查看效果,我们可以在项目的根目录下 运行 命令 node master.js 启动服务器,然后我们打开另一个命令行,执行 node tcp_client.js 启动客户端,然后我们会看到我们的10个请求被分发到不同的服务器上进行处理,如下所示:

3.3 Worker监听同一个端口

我们之前已经实现了句柄可以发送普通对象及socket对象外,我们还可以通过句柄的方式发送一个server对象。我们在master进程中创建一个TCP服务器,将服务器对象直接发送给worker进程,让worker进程去监听端口并处理请求。因此master进程和worker进程就会监听了相同的端口了。当我们的客户端发送请求时候,我们的master进程和worker进程都可以监听到,我们知道我们的master进程它是不会处理具体的业务的。

因此需要使用worker进程去处理具体的事情了。因此请求都会被worker进程处理了。

那么在这种模式下,主进程和worker进程都可以监听到相同的端口,当网络请求到来的时候,会进行抢占式调度,只有一个worker进程会抢到链接然后进行服务,由于是抢占式调度,可以理解为谁先来谁先处理的模式,因此就不能保证每个worker进程都能负载均衡的问题。下面是一个demo如下:

master.js 代码如下:

const childProcess = require('child_process');

const net= require('net');//获取cpu的数量

const cpuNum = require('os').cpus().length;

let workers=[];

let cur= 0;for (let i = 0; i < cpuNum; ++i) {

workers.push(childProcess.fork('./worker.js'));

console.log('worker process-' +workers[i].pid);

}//创建TCP服务器

const tcpServer =net.createServer();

tcpServer.listen(8989, () =>{

console.log('Tcp Server: 127.0.0.8989');//监听端口后将服务器句柄发送给worker进程

for (let i = 0; i < cpuNum; ++i) {

workers[i].send('tcpServer', tcpServer);

}//关闭master线程的端口监听

tcpServer.close();

});

worker.js 代码如下:

//接收主进程发来的消息

process.on('message', (msg, tcpServer) =>{if (msg === 'tcpServer' &&tcpServer) {

tcpServer.on('connection', (socket) =>{

setTimeout(()=>{

socket.end('Request handled by worker-' +process.pid);

},100);

})

}

});

tcp_client.js 代码如下:

const net = require('net');

const maxConnectCount= 10;for (let i = 0; i < maxConnectCount; ++i) {

net.createConnection({

port:8989,

host:'127.0.0.1'}).on('data', (d) =>{

console.log(d.toString());

})

}

如上代码,我们运行 node master.js 代码后,运行结果如下所示:

然后我们进行 运行 node tcp_client.js 命令后,运行结果如下所示:

如上我们可以看到 进程id为 37660 调度的比较多。

3.4 实现进程重启

worker进程可能会因为其他的原因导致异常而退出,为了提高集群的稳定性,我们的master进程需要监听每个worker进程的存活状态,当我们的任何一个worker进程退出之后,master进程能监听到并且能够重启新的子进程。在我们的Node中,子进程退出时候,我们可以在父进程中使用exit事件就能监听到。如果触发了该事件,就可以断定为子进程已经退出了,因此我们就可以在该事件内部做出对应的处理,比如说重启子进程等操作。

下面是我们上面监听同一个端口模式下的代码demo,但是我们增加了进程重启的功能。进程重启时,我们的master进程需要重新传递tcpServer对象给新的worker进程。但是master进程是不能被关闭的。否则的话,句柄将为空,无法正常传递。

master.js 代码如下:

const childProcess = require('child_process');

const net= require('net');//获取cpu的数量

const cpuNum = require('os').cpus().length;

let workers=[];

let cur= 0;for (let i = 0; i < cpuNum; ++i) {

workers.push(childProcess.fork('./worker.js'));

console.log('worker process-' +workers[i].pid);

}//创建TCP服务器

const tcpServer =net.createServer();/*服务器收到请求后分发给工作进程去处理*/tcpServer.on('connection', (socket) =>{

workers[cur].send('socket', socket);

cur= Number.parseInt((cur + 1) %cpuNum);

});

tcpServer.listen(8989, () =>{

console.log('Tcp Server: 127.0.0.8989');//监听端口后将服务器句柄发送给worker进程

for (let i = 0; i < cpuNum; ++i) {

workers[i].send('tcpServer', tcpServer);//监听工作进程退出事件

workers[i].on('exit', ((i) =>{return () =>{

console.log('worker-' + workers[i].pid + ' exited');

workers[i]= childProcess.fork('./worker.js');

console.log('Create worker-' +workers[i].pid);

workers[i].send('tcpServer', tcpServer);

}

})(i));

}//不能关闭master线程的,否则的话,句柄将为空,无法正常传递。

//tcpServer.close();

});

worker.js 代码如下:

//接收主进程发来的消息

process.on('message', (msg, tcpServer) =>{if (msg === 'tcpServer' &&tcpServer) {

tcpServer.on('connection', (socket) =>{

setTimeout(()=>{

socket.end('Request handled by worker-' +process.pid);

},100);

})

}

});

tcp_client.js 代码如下:

const net = require('net');

const maxConnectCount= 10;for (let i = 0; i < maxConnectCount; ++i) {

net.createConnection({

port:8989,

host:'127.0.0.1'}).on('data', (d) =>{

console.log(d.toString());

})

}

当我们在命令中 运行 node master.js  和 node tcp_client.js 执行后,如下图所示:

然后我们进入我们的电脑后台(我这边是mac电脑),进入活动监视器页面,结束某一个进程,如下图所示:

结束完成后,我们再来看下我们的 node master.js 命令可以看到,先打印 某某工作进程被退出了,然后某某工作进程被创建了,如下图所示

然后我们再到我们的 活动监视器可以看到新的 进程号被加进来了,如下图所示:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值