首先我们要知道一点,node 是单线程执行的,但是如今的计算机已经发展到了多核CPU,因此,要解决node利用多核CPU服务器就成了一个重要的问题。
一. 多进程的架构分析:其中在 master.js 文件夹下,通过 child_process 来复制子进程,os 来访问CPU个数,来控制复制多少子进程,同时子进程中开启了 3000~4000 随机端口的监听。
// master.js
let fork = require('child_process').fork;
let cpus = require('os').cpus();
for(let i = 0; i< cpus.length; i ++){
fork('./worker.js');
}
// worker.js
let http = require('http');
http.createServer((req, res) => {
res.writeHead(200,{'Content-Type':'text/plain'});
res.end('hello word');
}).listen(Math.round((3 + Math.random()) * 1000 ) , '127.0.0.1');
1. 创建子进程
| spawn | 启动子进程来执行命令 |
| exec | 启动一个子进程来执行命令,但是它有一个回调函数获知子进程状况 |
| execFile | 启动一个子进程来执行可执行文件 |
| fork | 与 spawn 类似,不同点在于他创建 Node 子进程只需要指定要执行的js文件模块即可 |
详细使用请查看官方文档:child_process 创建
2. 进程间的通信
分析:父子进程,通过 on 来监听消息传入,send 来发送消息,他们之间通信会创建 IPC 通道,通过该通道,才能实现父子进程的消息传递。
// master.js 主进程
let cp = require('child_process');
let worker = cp.fork('./worker.js');
// 用于监听子进程传来的信息
worker.on('message', ( data ) =>{
console.log('收到子进程发送的消息: ', data);
})
// 用于向子进程发送信息
worker.send('你好,子进程 ');
// worker.js 工作进程
// 3 秒后向主进程发送信息
setTimeout(()=>{
process.send({name: '石头山'});
},3000)
// 用来监听父进程的消息
process.on('message', (data) => {
console.log('我收到了', data);
})
进程间通信原理
IPC 全称是 Inter-Process Communication ,即进程间通信,进程间的通信目的是为了让不同的进程能够互相访问资源并进行协调工作。
父进程在创建子进程之前,会先创建IPC通道并监听它,然后才真正创建子进程,并通过环境变量告诉子进程这个IPC 通道的文件描述符(文件描述符解释)。子进程在启动过程中,根据文件描述符去连接这个已经存在的IPC通道,从而完成父子进程之间的连接。它与 socket 的行为比较类似,但是省去了网络层,直接在系统内核完成,比较高效。
3. 句柄传递
首先我们引入一个概念,句柄,什么是句柄?句柄就是一种用来标识资源的引用,它的内部包含了指向对象的文件描述符。比如句柄可以用来标识一个服务器 socket对象,一个客户端socket对象,一个UDP套接字,一个管道。
我们想象这么一个场景,比如某个接口在同一时间有大量用户访问,而此时单线程的 node 显得比较吃力,虽然我们可以利用多进程的特性,但是如何让多个进程同时监听同一个端口,就成了要给难题,而这就需要用到我们所说的句柄传递功能。
分析:主进程将 server 传递到子进程,启动监听后关闭 server,从而实现了子进程直接对端口的监听,每次请求随机被某个子进程响应,它们之间实现响应是抢占式的。
// parent.js 启动主进程
let cp = require('child_process');
let cpus = require('os').cpus
let child2 = cp.fork('child2.js');
let child1 = cp.fork('child1.js');
console.log(cpus)
let server = require('net').createServer();
server.on('connection', socket => {
socket.end('handled by parent');
})
server.listen(1337, () => {
child1.send('server1', server);
child2.send('server2', server);
server.close();
})
// child1 进程
let http = require('http');
let server = http.createServer((req, res ) => {
if(req.url === '/') {
res.writeHead(200, {'Content-Type':'text/plain'});
res.end('handled by child1, pid is' + process.pid + 'n');
}
})
process.on('message', ( m, tcp ) => {
if( m === 'server1') {
tcp.on('connection', socket => {
server.emit('connection', socket);
})
}
})
// child2 进程
let http = require('http');
let server = http.createServer((req, res ) => {
if(req.url === '/index'){
res.writeHead(200, {'Content-Type':'text/plain'});
res.end('handled by child2, pid is' + process.pid + 'n');
}
})
process.on('message', ( m, tcp ) => {
console.log(22222)
if( m === 'server2') {
tcp.on('connection', socket => {
server.emit('connection', socket);
})
}
})
二. 集群稳定之路
充分利用了多核 CPU 的资源,就可以迎接 大量客户端请求了,但是我们还需要考虑很多问题,性能,多个进程存活状态,进程重启等等一系列问题。
1. 进程事件
详细请看官方文档:进程事件
| error | 当子进程无法被复制创建、无法被杀死、无法发送消息时触发 |
| exit | 子进程退出时触发该事件 |
| close | 在子进程标准输入输出流终止时触发 |
| disconnect | 在父进程或子进程中调用disconnect() 方法时触发,并关闭IPC通道 |
2. 自动重启
分析:通过 os 文件来获取电脑 cpu 核数,以此来创建最多的工作进程,并且通过监听工作进程的 emit 事件,一旦有进程退出,便立刻启动新的进程,保证整个集群中总是有进程在为用户服务的。
let fork = require('child_process').fork;
let cpus = require('os').cpus();
let server = require('net').createServer();
server.listen(1337);
let workers = {};
let createWorker = function (i) {
let worker = fork(__dirname + `/worker${i}.js`);
// 退出时重新启动新的进程
worker.on('exit', () => {
console.log('Worker' + worker.pid + 'exited');
delete workers[worker.pid];
createWorker();
})
// 句柄转发
worker.send('server' + i, server);
console.log('Create worker.pid = ' + worker.pid);
};
for(let i = 0; i< cpus.length; i++) {
createWorker(i);
}
// 进程退出时让所有工作进程退出
process.on('exit', ()=>{
for(let pid in workers){
workers[pid].kill();
}
})
自杀信号
分析:进程监听未捕获异常,一旦出现未捕获的异常,则向主进程发送一个 suicide 信号,主进程收到该信号,知道有一个进程停止工作,便重新启动一个新的进程。
// 自杀信号
process.on('uncaughtException', () => {
process.send({act: 'suicide'});
// 停止接收新的信号
worker.close(()=>{
// 所有已有连接断开后退出进程
process.exit(1);
})
});
3. 负载均衡
在多个进程之间监听相同的端口,使得用户请求能够分散到多个进程上进行处理,这可以使 CPU 资源完全的都调用起来。防止某个进程一直工作,而有的进程停滞,Node 默认的机制是采用操作系统的抢占式策略。这种策略可能造成负载不均衡。因此,Node v0.11 提出了一种新的策略,调度(Round-Robin),其工作方式是由主进程接受连接,将其依次发放给工作进程,分发的策略是在N个工作进程中,每次选择第 i = ( i + 1 )mod n 个进程来发送连接。
4. 状态共享
我们知道,在 Node 进程中不宜放置太多数据,因为他会加重垃圾回收的负担,进而影响性能。同时 Node 也不允许在多个进程之间共享数据。但实际业务中往往不可避免,因此我们需要使用一种方案和机制来实现多个进程之间的数据共享。
第三方数据存储:数据库、磁盘文件、缓存服务 Redis 中定时轮询:各子进程向第三方定时轮询主动通知:当数据发生改变,主进程依次通知各个子进程 三. Cluster 模块
Cluster 是Node中处理多核CPU的一个成熟的模块,在V0.8 之前,都是使用 process_child 来实现。在其之后,使用Cluster 可以很好的解决其问题,它提供了比较完善的 API ,用以处理进程的健壮性问题。
分析:通过 cluster.isMaster 来判断是否为主进程,进而来创建工作进程或者创建监听。
let cluster = require('cluster');
let http = require('http');
const { off } = require('process');
let numCPUs = require('os').cpus().length;
if(cluster.isMaster){
for(let i = 0; i{
console.log('worker' + worker.process.pid + 'died');
});
}else{
http.createServer((req, res)=>{
res.writeHead(200);
res.end('hellow world'+ process.pid + 'n');
}).listen(8000)
}
// cluster.setupMaster({
// exec: 'worker.js'
// })
四. 总结
群体的力量是强大的,通过主从模式,将应用的质量提升了一个档次。在设计中,每个子进程应当只做一件事,并且做好一件事,通过进程间的技术将他们连接起来,将简单组合成强大。



