nodejs玩儿转进程

序言node

  1. nodejs是如何充分利用多核cup 服务器的?
  2. 如何保证进程的稳健型?

正文缓存

由于Node运行在V8引擎上,咱们的JavaScript 将会运行在单个进程的单个线程上。它带来的好处是: 程序状态是单一的,在没有多线程的状况 下没有锁、线程同步问题,操做系统在调度时也由于较少上下文的切换,能够很好地提升CPU的使用率bash

从严格的意义上而言,Node并不是真正的单线程架构,Node自身还有 必定的I/O线程存在,这些I/O线程由底层libuv处理,这部分线程对于JavaScript开发者而言是透明 的,只在C++扩展开发时才会关注到服务器

进程和线程的区别及优劣:
1进程是操做系统分配资源的最小单元
复制代码

多进程的缺点主要体如今网络

1 没法共享内部状态(进程池的方式能够解决)
	2 以及建立和销毁进程时候
复制代码

多线程相对多进程的优势:多线程

建立和销毁线程相对进程来讲开销小不少,(而且线程之间能够共享数据 ,内存浪费的问题得
以解决)而且利用线程池能够减小建立和销毁线程的开销
复制代码

多线程的缺点:架构

每一个线程都有本身独立的堆栈,每一个堆栈都要占用必定的内存空间
复制代码

服务模型的变迁:

从“古”到今,Web服务器的架构已经历了几回变迁。从服务器处理客户端请求的并发量这个纬度来看,每次变迁都是里程碑的见证并发

由此来看 多线程和事件驱动都有本身弊端

事件驱动:CPU的计算能力决定这类服务的性能上线
多线程模式:受资源上限的影响
复制代码

那么nodejs是如何充分利用多核cup 服务器的?

答案是经过fork进程的方式 ,咱们再一次将经典的示例代码存为worker.js文件,代码以下:负载均衡

var http = require('http'); http.createServer(function (req, res) {

	res.writeHead(200, {'Content-Type': 'text/plain'});
	res.end('Hello World\n');
	
}).listen(Math.round((1 + Math.random()) * 1000), '127.0.0.1');
复制代码

经过node worker.js启动它,将会侦听1000到2000之间的一个随机端口 将如下代码存为master.js,并经过node master.js启动它:dom

/** * 充分利用cup的资源同时启动在多个进程上启动服务 */
const cpus = require("os").cpus();
const fork = require("child_process").fork;

for (let index = 0; index < cpus.length; index++) {
  fork("./worker.js");
}
复制代码

这段代码将会根据当前机器上的CPU数量复制出对应Node进程数。在*nix系统下能够经过ps aux | grep worker.js查看到进程的数量,以下所示

建立子进程

child_process 模块赋予了node能够随意建立子进程的能力 ,它提供了4个方法用于建立子进程:

spawn(): 启动一个子进程来执行命令 exec: 启动一个子进程来执行命令,与spawn不一样的是其接口不一样,他有一个回掉函数来获知子进程的情况。 execFile():启动一个子进程来执行可执行文件。 fork():与spawn()相似,不一样点在于它建立Node的子进程只需指定要执行的JavaScript文件模块便可。

spawn()与exec()、execFile()的不一样是:
后二者建立时能够指定timeout属性设置超时时间,一旦建立的进程运行超过设定的时间将会
被杀死。
复制代码

exec()与execFile()不一样的是,exec()适合执行已有的命令execFile()适合执行文件。这里咱们以一个寻常命令为例,node worker.js分别用上述4种方法实现,以下所示

var cp = require('child_process');
//spawn
cp.spawn('node', ['worker.js']);
//exec
cp.exec('node worker.js', function (err, stdout, stderr) {
    // some code 
});
//execFile
cp.execFile('worker.js', function (err, stdout, stderr) { 
	// some code
}); 
//fork
cp.fork('./worker.js');
复制代码

若是是JavaScript文件经过execFile()运行,它的首行内容必须添加以下代码

#!/usr/bin/env node
复制代码

尽管4种建立子进程的方式有些差异,但事实上后面3种方法都是spawn()的延伸应用

进程间通讯

主线程与工做线程之间经过onmessage()和postMessage()进行通讯,子进程对象则由send() 方法实现主进程向子进程发送数据message事件实现收听子进程发来的数据,与API在必定 程度上类似。经过消息传递内容,而不是共享或直接操做相关资源,这是较为轻量和无依赖 的作法

parent.js

// 
var cp = require('child_process');
var n = cp.fork(__dirname + '/sub.js');
n.on('message', function (m) { console.log('PARENT got message:', m);
});
n.send({hello: 'world'});
复制代码

sub.js

process.on('message', function (m) { 
	console.log('CHILD got message:', m);
});
process.send({foo: 'bar'});
复制代码

经过fork()或者其余API,建立子进程以后,为了实现父子进程之间的通讯,父进程与子进程之间将会建立IPC通道。经过IPC通道,父子进程之间才能经过message和send()传递消息

进程间通讯原理

IPC的全称是Inter-Process Communication,即进程间通讯 进程间通讯的目的是为了让不一样的进程可以互相访问资源并进行协调工做

实现进程间通讯的技术有不少,如 命名管道 匿名管道 socket 信号量 共享内存 消息队列 Domain Socket

Node中实现IPC通道的是管道(pipe) 技术。但此管道非彼管道,在Node中管道是个抽象层面的称呼,具体细节实现由libuv提供,在 Windows下由命名管道(named pipe)实现,*nix系统则采用Unix Domain Socket实现。表如今应用层上的进程间通讯只有简单的message事件和send()方法,接口十分简洁和消息化。下图为IPC 建立和实现的示意图。

父进程在实际建立子进程以前,会建立IPC通道并监听它,而后才真正建立出子进程并通 过环境变量(NODE_CHANNEL_FD)告诉子进程这个IPC通道的文件描述符。子进程在启动的过程当中, 根据文件描述符去链接这个已存在的IPC通道,从而完成父子进程之间的链接

句柄传递

创建好进程之间的IPC后,若是仅仅只用来发送一些简单的数据,显然不够咱们的实际应用 使用 若是让服务都监听 到相同的端口,将会有什么样的结果?

这时只有一个工做进程可以监听到该端口上,其他的进程在监听的过程当中都抛出了 EADDRINUSE异常,这是端口被占用的状况,新的进程不能继续监听该端口了。这个问题破坏了我 们将多个进程监听同一个端口的想法。要解决这个问题,一般的作法是让每一个进程监听不一样的端 口,其中主进程监听主端口(如80),主进程对外接收全部的网络请求,再将这些请求分别代理 到不一样的端口的进程上。示意图如图9-4所示。

经过代理,能够避免端口不能重复监听的问题,甚至能够在代理进程上作适当的负载均衡, 使得每一个子进程能够较为均衡地执行任务。因为进程每接收到一个链接,将会用掉一个文件描述 符,所以代理方案中客户端链接到代理进程, 代理进程链接到工做进程的过程须要用掉两个文件 描述符。操做系统的文件描述符是有限的,代理方案浪费掉一倍数量的文件描述符 的作法影响了 系统的扩展能力

主进程代码以下所示
var child = require('child_process').fork('child.js');
// Open up the server object and send the handle 
var server = require('net').createServer(); 
server.on('connection', function (socket) {
	socket.end('handled by parent\n'); 
});
server.listen(1337, function () { 
	child.send('server', server);
});
复制代码
子进程代码以下所示:
process.on('message', function (m, server) { 
	if (m === 'server') {
		server.on('connection', function (socket) { 
			socket.end('handled by child\n');
		}); 
	}
});
复制代码

而后新开一个命令行窗口,用上curl工具,以下所示:

$ curl "http://127.0.0.1:1337/" 
handled by parent
$ curl "http://127.0.0.1:1337/"
handled by child
$ curl "http://127.0.0.1:1337/" 
handled by child
$ curl "http://127.0.0.1:1337/" 
handled by parent
复制代码

命令行中的响应结果也是很难以想象的,这里子进程和父进程都有可能处理咱们客户端发起 的请求。 试试将服务发送给多个子进程,以下所示: parent.js

var cp = require('child_process'); 
var child1 = cp.fork('child.js'); 
var child2 = cp.fork('child.js');
// Open up the server object and send the handle 
var server = require('net').createServer(); 
server.on('connection', function (socket) {
	socket.end('handled by parent\n'); 
});
server.listen(1337, function () { 
	child1.send('server', server); 		
	child2.send('server', server);
});

复制代码

而后在子进程中将进程ID打印出来,以下所示: // child.js

process.on('message', function (m, server) { 
	if (m === 'server') {
		server.on('connection', function (socket) {
			socket.end('handled by child, pid is ' + process.pid + '\n');
		}); 
	}
});
复制代码

再用curl测试咱们的服务,以下所示:

$ curl "http://127.0.0.1:1337/" 
handled by child, pid is 24673 
$ curl "http://127.0.0.1:1337/" 
handled by parent
$ curl "http://127.0.0.1:1337/" 
handled by child, pid is 24672
复制代码

测试的结果是每次出现的结果均可能不一样,结果可能被父进程处理,也可能被不一样的子进程 处理。 其实咱们能够在父进程启动以后立马把他close掉

const cp = require("child_process")
const child1 = cp.fork("child.js");
const child2 = cp.fork('child.js');
const server = require("net").createServer();

server.on("connection",(socket)=>{
  socket.end('handled by parent\n');
})

server.listen(1337,()=>{
  child2.send('server', server);
  child1.send('server', server);
  server.close();
})
复制代码

整个过程当中,服务的过程发生了一次改变,如

主进程发送完句柄并关闭监听以后成为了下图所示的结构

咱们神奇地发现, 多个子进程能够同时监听相同端口,再没有EADDRINUSE异常发生了

句柄发送与还原

句柄发送跟咱们直接将服务器对象发送给子进程有没有差异?它是否真的将服务器对象发送给了子进程?为何它能够发送到多个子进程 中?发送给子进程为何父进程中还存在这个对象? 目前子进程对象send()方法能够发送的句柄类型包括以下几种。

  • net.Socket。TCP套接字。
  • net.Server。TCP服务器,任意创建在TCP服务上的应用层服务均可以享受到它带来的好处。
  • net.Native。C++层面的TCP套接字或IPC管道。
  • dgram.Socket。UDP套接字。
  • dgram.Native。C++层面的UDP套接字。

send()方法在将消息发送到IPC管道前,将消息组装成两个对象,一个参数是handle,另外一个 是message。message参数以下所示

{
	cmd: 'NODE_HANDLE',
	type: 'net.Server', 
	msg: message
}
复制代码

发送到IPC管道中的其实是咱们要发送的句柄文件描述符,文件描述符其实是一个整数 值。这个message对象在写入到IPC管道时也会经过JSON.stringify()进行序列化。因此最终发送 到IPC通道中的信息都是字符串,send()方法能发送消息和句柄并不意味着它能发送任意对象。

链接了IPC通道的子进程能够读取到父进程发来的消息,将字符串经过JSON.parse()解析还 原为对象后,才触发message事件将消息体传递给应用层使用。在这个过程当中,消息对象还要被 进行过滤处理,message.cmd的值若是以NODE_为前缀,它将响应一个内部事件internalMessage。

若是message.cmd值为NODE_HANDLE,它将取出message.type值和获得的文件描述符
 一块儿还原出一个对应的对象。这个过程的示意图如图所示
复制代码

以发送的tcp服务器句柄为例 子进程收到消息后的还原过程以下

function(message, handle, emit) {
 var self = this;
var server = new net.Server(); 
server.listen(handle, function() {
	emit(server); 4 });
}
复制代码

上面的代码中,子进程根据message.type建立对应TCP服务器对象,而后监听到文件描述符上。因为底层细节不被应用层感知,因此在子进程中,开发者会有一种服务器就是从父进程中直接传递过来的错觉。值得注意的是,Node进程之间只有消息传递,不会真正地传递对象,这种错 觉是抽象封装的结果

端口共同监听

多个进程能够监听到 相同的端口而不引发EADDRINUSE异常?

咱们独立启动的进程中,TCP服务器端socket套接字的文件描述符并不相同,致使监听到相同的端口时会抛出异常。

Node底层对每一个端口监听都设置了SO_REUSEADDR选项,这个选项的涵义是不一样进程能够就相 同的网卡和端口进行监听,这个服务器端套接字能够被不一样的进程复用,以下所示

setsockopt(tcp->io_watcher.fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))
复制代码

因为独立启动的进程互相之间并不知道文件描述符,因此监听相同端口时就会失败。但对于 send()发送的句柄还原出来的服务而言,它们的文件描述符是相同的,因此监听相同端口不会引 起异常

多个应用监听相同端口时,文件描述符同一时间只能被某个进程所用。换言之就是网络请求 向服务器端发送时,只有一个幸运的进程可以抢到链接,也就是说只有它能为这个请求进行服务。 这些进程服务是抢占式的。

集群稳定之路

搭建好了集群,充分利用了多核CPU资源,彷佛就能够迎接客户端大量的请求了。但请等等, 咱们还有一些细节须要考虑。

  • 性能问题。
  • 多个工做进程的存活状态管理。
  • 工做进程的平滑重启。
  • 配置或者静态数据的动态从新载入。
  • 其余细节。

进程事件

再次回归到子进程对象上,除了引人关注的send()方法和message事件外,子进程还有些什 么呢?首先除了message事件外,Node还有以下这些事件:

error:当子进程没法被复制建立、没法被杀死、没法发送消息时会触发该事件 exit:子进程退出时触发该事件,子进程若是是正常退出,这个事件的第一个参数为退出 码,不然为null。若是进程是经过kill()方法被杀死的,会获得第二个参数,它表示杀死进程时的信号。 close:在子进程的标准输入输出流停止时触发该事件,参数与exit相同 disconnect:在父进程或子进程中调用disconnect()方法时触发该事件,在调用该方法时将关闭监听IPC通道。

自动重启

有了父子进程之间的相关事件以后,就能够在这些关系之间建立出须要的机制了。至少咱们 可以经过监听子进程的exit事件来获知其退出的信息,接着前文的多进程架构,咱们在主进程上 要加入一些子进程管理的机制,好比从新启动一个工做进程来继续服务。

实现代码以下所示: master.js

// 主进程
const fork = require("child_process").fork;
const cpus = require("os").cpus();

// 建立tcp server
const server = require("net").createServer();
server.listen(1337);

const workers = {}

// 建立进程的函数
const createWorker = () =>{
  const worker = fork(__dirname+"/work.js");
  // 监听进程退出事件 自动重启
  worker.on("exit",()=>{
    delete workers[worker.pid];
    console.log(worker.pid+"is delete");
    createWorker();
  })
  // 发送当前进程的句柄文件描述符
  worker.send("server",server)
  workers[worker.pid]= worker
  console.log('created worker :', worker.pid);
}

for (let index = 0; index < cpus.length; index++) {
  createWorker();
}

// 进程本身退出时,让全部工做进程退出 
process.on('exit', function () {
  for (var pid in workers) { 
    workers[pid].kill();
  } 
});

console.log('process.pid', process.pid)
复制代码

work.js

var http = require('http');
var server = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('handled by child, pid is' + process.pid + '\n'); 
});

var worker;
process.on('message', function (m, tcp) {
  if (m === 'server') {
    worker = tcp;
    worker.on('connection', function (socket) {
      server.emit('connection', socket); 
    });
  } 
});

process.on('uncaughtException', function () { // 中止接收新的链接
  worker.close(function () {
    // 全部已有链接断开后,退出进程
    process.exit(1); 
  });
});
复制代码

测试一下上面的代码,以下所示:

$ node master.js
Create worker: 30504 
Create worker: 30505 
Create worker: 30506 
Create worker: 30507
复制代码

上述代码的处理流程是,一旦有未捕获的异常出现,工做进程就会当即中止接收新的链接; 当全部链接断开后,退出进程。主进程在侦听到工做进程的exit后,将会当即启动新的进程服务, 以此保证整个集群中老是有进程在为用户服务的。 经过kill命令杀死某个进程试试,以下所示

$ kill 30506
复制代码

结果是30506进程退出后,自动启动了一个新的工做进程30518,整体进程数量并无发生改 变,以下所示:

Worker 30506 exited. 
Create worker. pid: 30518
复制代码

自杀信号

固然上述代码存在的问题是要等到已有的全部链接断开后进程才退出,在极端的状况下,所 有工做进程都中止接收新的链接,全处在等待退出的状态。但在等到进程彻底退出才重启的过程 中,全部新来的请求可能存在没有工做进程为新用户服务的情景,这会丢掉大部分请求。

为此须要改进这个过程,不能等到工做进程退出后才重启新的工做进程。固然也不能暴力退 出进程,由于这样会致使已链接的用户直接断开。因而咱们在退出的流程中增长一个自杀 (suicide)信号。工做进程在得知要退出时,向主进程发送一个自杀信号,而后才中止接收新的 链接,当全部链接断开后才退出。主进程在接收到自杀信号后,当即建立新的工做进程服务。 代码改动以下所示:

// master.js 主要是重启进程的任务放到了 接收到suicide 事件以后
// 主进程
const fork = require("child_process").fork;
const cpus = require("os").cpus();

// 建立tcp server
const server = require("net").createServer();
server.listen(1337);

const workers = {}

// 建立进程的函数
const createWorker = () =>{
  const worker = fork(__dirname+"/work.js");
  // 启动新的进程
  worker.on('message', function (message) {
    if (message.act === 'suicide') { 
      createWorker();
    } 
  });
  
  worker.on("exit",()=>{
    delete workers[worker.pid];
    console.log(worker.pid+"is delete");
  })
  // 发送当前进程的句柄文件描述符
  worker.send("server",server)
  workers[worker.pid]= worker
  console.log('created worker :', worker.pid);
}

for (let index = 0; index < cpus.length; index++) {
  createWorker();
}

// 进程本身退出时,让全部工做进程退出 
process.on('exit', function () {
  for (var pid in workers) { 
    workers[pid].kill();
  } 
});

console.log('process.pid', process.pid)
复制代码

work.js主要是再接收到未捕获的异常以后向主进程发送事件告知子进程将要退出 此时建立新的进程为用户服务 ,以后子进程才退出 再回头看重启信息,以下所示:

created worker : 14397
14394is delete
复制代码

与前一种方案相比,建立新工做进程在前,退出异常进程在后。在这个可怜的异常进程退出 以前,老是有新的工做进程来替上它的岗位。至此咱们完成了进程的平滑重启,一旦有异常出现, 主进程会建立新的工做进程来为用户服务,旧的进程一旦处理完已有链接就自动断开。整个过程 使得咱们的应用的稳定性和健壮性大大提升。示意图如图所示

这里存在问题的是有可能咱们的链接是长链接,不是HTTP服务的这种短链接,等待长链接 断开可能须要较久的时间。为此为已有链接的断开设置一个超时时间是必要的,在限定时间里强 制退出的设置以下所示:

process.on('uncaughtException', function (err) {
process.send({act: 'suicide'}); 2 // 中止接收新的链接
   worker.close(function () {
   	// 全部已有链接断开后,退出进程
   	process.exit(1);
   }); // 5秒后退出进程
   setTimeout(function () {
     process.exit(1); 
   }, 5000);
});
复制代码

进程中若是出现未能捕获的异常,就意味着有那么一段代码在健壮性上是不合格的。为此退 出进程前,经过日志记录下问题所在是必需要作的事情,它能够帮咱们很好地定位和追踪代码异 常出现的位置,以下所示:

process.on('uncaughtException', function (err) { // 记录日志
	logger.error(err);
	// 发送自杀信号
	process.send({act: 'suicide'}); // 中止接收新的链接 
	worker.close(function () {
	// 全部已有链接断开后,退出进程
		process.exit(1); 
	});
	// 5秒后退出进程 
	setTimeout(function () {
		process.exit(1);
	}, 5000);
});

复制代码

经过自杀信号告知主进程可使得新链接老是有进程服务,可是依然仍是有极端的状况。工 做进程不能无限制地被重启,若是启动的过程当中就发生了错误,或者启动后接到链接就收到错误, 会致使工做进程被频繁重启,这种频繁重启不属于咱们捕捉未知异常的状况,由于这种短期内 频繁重启已经不符合预期的设置,极有多是程序编写的错误。 为了消除这种无心义的重启,在知足必定规则的限制下,不该当反复重启。好比在单位时间 内规定只能重启多少次,超过限制就触发giveup事件,告知放弃重启工做进程这个重要事件。 为了完成限量重启的统计,咱们引入一个队列来作标记,在每次重启工做进程之间进行打点 并判断重启是否太过频繁,以下所示:

// 重启次数 
var limit = 10;
// 时间单位
var during = 60000;
var restart = [];
var isTooFrequently = function () {
	// 记录重启时间
	var time = Date.now();
	var length = restart.push(time);
	if (length > limit) {
		// 取出最后10个记录
		restart = restart.slice(limit * -1);
	}
	// 最后一次重启到前10次重启之间的时间间隔
	return restart.length >= limit && restart[restart.length - 1] - restart[0] < during; 
};
var workers = {};
var createWorker = function () {
	// 检查是否太过频繁
	if (isTooFrequently()) {
	// 触发giveup事件后,再也不重启 
		process.emit('giveup', length, during);
		return;
	}
	var worker = fork(__dirname + '/worker.js'); 
	worker.on('exit', function () {
		console.log('Worker ' + worker.pid + ' exited.');
		delete workers[worker.pid]; 
	});
	// 从新启动新的进程
	worker.on('message', function (message) {
		if (message.act === 'suicide') {
		 createWorker();
		} 
	});
// 句柄转发
worker.send('server', server); workers[worker.pid] = worker;
console.log('Create worker. pid: ' + worker.pid);
};
复制代码

giveup事件是比uncaughtException更严重的异常事件。uncaughtException只表明集群中某个 工做进程退出,在总体性保证下,不会出现用户得不到服务的状况,可是这个giveup事件则表示 集群中没有任何进程服务了,十分危险。为了健壮性考虑,咱们应在giveup事件中添加剧要日志, 并让监控系统监视到这个严重错误,进而报警等。

负载均衡

在多进程之间监听相同的端口,使得用户请求可以分散到多个进程上进行处理,这带来的好 处是能够将CPU资源都调用起来。这犹如饭店将客人的点单分发给多个厨师进行餐点制做。既然 涉及多个厨师共同处理全部菜单,那么保证每一个厨师的工做量是一门学问,既不能让一些厨师忙不过来,也不能让一些厨师闲着,这种保证多个处理单元工做量公平的策略叫负载均衡Node默认提供的机制是采用操做系统的抢占式策略。所谓的抢占式就是在一堆工做进程中,闲着的进程对到来的请求进行争抢,谁抢到谁服务。

通常而言,这种抢占式策略对你们是公平的,各个进程能够根据本身的繁忙度来进行抢占。对于Node而言,它的繁忙是由CPU、I/O两个部分构成的影响抢占的是CPU 的繁忙度对不一样的业务,可能存在I/O繁忙,而CPU较为空闲的状况,这可能形成某个进程能 够抢到较多请求,造成负载不均衡的状况

为此Node在v0.11中提供了一种新的策略使得负载均衡更合理,这种新的策略叫 Round-Robin,又叫轮叫调度。轮叫调度的工做方式是由主进程接受链接,将其依次分发给工做 进程。分发的策略是在N个工做进程中,每次选择第i = (i + 1) mod n个进程来发送链接。在cluster 模块中启用它的方式以下:

状态共享

Node进程中不宜存放太多数据,由于它会加剧垃圾回收的负担,进 而影响性能。同时,Node也不容许在多个进程之间共享数据。但在实际的业务中,每每须要共享 一些数据,譬如配置数据,这在多个进程中应当是一致的。为此,在不容许共享数据的状况下, 咱们须要一种方案和机制来实现数据在多个进程之间的共享。

  1. 第三方数据存储 解决数据共享最直接、简单的方式就是经过第三方来进行数据存储,好比将数据存放到数据 库、磁盘文件、缓存服务(如Redis)中,全部工做进程启动时将其读取进内存中。但这种方式 存在的问题是若是数据发生改变,还须要一种机制通知到各个子进程,使得它们的内部状态也得 到更新。 实现状态同步的机制有两种,一种是各个子进程去向第三方进行定时轮询,示意图如图所示。

实现状态同步的机制有两种:

一种是各个子进程去向第三方进行定时轮询
复制代码

定时轮询带来的问题是轮询时间不能过密,若是子进程过多,会造成并发处理,若是数据没 有发生改变,这些轮询会没有意义,白白增长查询状态的开销。若是轮询时间过长,数据发生改 变时,不能及时更新到子进程中,会有必定的延迟。 2. 主动通知 一种改进的方式是当数据发生更新时,主动通知子进程。固然,即便是主动通知,也须要一 种机制来及时获取数据的改变。这个过程仍然不能脱离轮询,但咱们能够减小轮询的进程数量, 咱们将这种用来发送通知和查询状态是否更改的进程叫作通知进程。为了避免混合业务逻辑,能够 将这个进程设计为只进行轮询和通知,不处理任何业务逻辑,示意图如图所示

这种推送机制若是按进程间信号传递,在跨多台服务器时会无效,是故能够考虑采用TCP或 UDP的方案。进程在启动时从通知服务处除了读取第一次数据外,还将进程信息注册到通知服务 处。一旦经过轮询发现有数据更新后,根据注册信息,将更新后的数据发送给工做进程。因为不涉及太多进程去向同一地方进行状态查询,状态响应处的压力不至于太过巨大,单一的通知服务 轮询带来的压力并不大,因此能够将轮询时间调整得较短,一旦发现更新,就能实时地推送到各个子进程中。

Cluster 模块 v0.8时直接引入了cluster模块,用以解决多核CPU的利用率问题,同时也提供了较完 善的API,用以处理进程的健壮性问题 对于开头提到的建立Node进程集群,cluster实现起来也是很轻松的事情,以下所示

// 事实上cluster模块就是child_process和net模块的组合应用
const cluster = require("cluster");
const cpus = require('os').cpus();

cluster.setupMaster({
  exec: "worker.js"
})

for(var i = 0; i < cpus.length; i++) {
  cluster.fork();
}

复制代码

Cluster 事件

对于健壮性处理,cluster模块也暴露了至关多的事件。

fork:复制一个工做进程后触发该事件。
online:复制好一个工做进程后,工做进程主动发送一条online消息给主进程,主进程收到消息后,触发该事件。
listening:工做进程中调用listen()(共享了服务器端Socket)后,发送一条listening消息给主进程,主进程收到消息后,触发该事件。
disconnect:主进程和工做进程之间IPC通道断开后会触发该事件。
exit:有工做进程退出时触发该事件。
setup:cluster.setupMaster()执行后触发该事件。
复制代码

这些事件大多跟child_process模块的事件相关,在进程间消息传递的基础上完成的封装。 这些事件对于加强应用的健壮性已经足够了 尽管经过child_process模块能够大幅提高Node的稳定性,可是一旦主进程出现问题,所 有子进程将会失去管理。在Node的进程管理以外,还须要用监听进程数量或监听日志的方式确 保整个系统的稳定性,即便主进程出错退出,也能及时获得监控警报,使得开发者能够及时处 理故障

相关文章
相关标签/搜索