深刻浅出nodeJS - 4 - (玩转进程、测试、产品化)

内容

9.玩转进程
10.测试
11.产品化

1、玩转进程

node的单线程只不过是js层面的单线程,是基于V8引擎的单线程,由于,V8的缘故,先后端的js执行模型基本上是相似的,可是node的内核机制依然是经过libuv调用epoll或者IOCP的多线程机制。换句话说,node从严格意义上讲,并不是是真正的单线程架构,node内核自身有必定的IO线程和IO线程池,经过libuv的调度,直接使用了操做系统层面的多线程。node的开发者,能够经过扩展c/c++模块来直接操纵多线程来提升效率。不过,单线程带来的好处是程序状态单一,没有锁、线程同步、线程上下文切换等问题。可是单线程的程序,并不是是完美的。如今的服务器不少都是多cpu,多cpu核心的,一个node实例只能利用一个cpu核心,那么其余的cpu核心不就浪费了吗?而且,单线程的容错也很弱,一旦抛出了没有捕获的异常,必将引发整个程序的崩溃,那这样的程序必然是很是脆弱的,这样的服务器端语言又有什么价值呢?node

两个问题:c++

  1. 如何让node充分利用多核cpu服务器?
  2. 如何保证node进程的健壮性和稳定性?

1.服务模型的变迁

经历了同步(qps为1/n)、复制进程(预先赋值必定数量的进程,prefork,可是,一旦用超了,仍是跟同步的服务器同样,qps为m/n)、多线程(qps为M*L/N,这种模型,当并发上万后,内存耗用的问题将会暴露出来也就是C10k问题,apache就是采用了这样的多线程、多进程架构)和事件驱动等几个不一样的模型。apache

2.多进程架构

面对单进程单线程对多核使用不足的问题,前人的经验是启动多个进程,理想状态下,每一个进程各自利用一个cpu,以此实现多核cpu的利用。node提供了child_process模块,并提供了child_process.fork()函数来实现进程的复制。后端

//node 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 master.js
var fork = require('child_process').fork;
var cpus = require('os').cpus();
for (var i = 0; i < cpus.length; i++) {
fork('./worker.js');
}

这两段代码会根据当前机器上的cpu数量,复制出对应node进程数,在*nix下,能够经过ps aux | grep worker.js查看到进程的数量。
这就是主从架构了,在这里存在两个进程,master是主进程、worker是工做进程。这是典型的分布式架构用于并行业务处理的模式,具备较好的可伸缩性和稳定性。主进程不负责具体业务处理,只负责调度和管理工做进程,所以主进程是相对于稳定和简单的,工做进程负责具体的业务处理,由于,业务多种多样,因此,工做进程的稳定性,是咱们须要考虑的。api

clipboard.png

经过fork复制的进程都是独立的,每一个进程都有着独立而全新的v8实例,所以,须要至少30毫秒的启动时间和10mb左右的内存,可是,咱们要记得fork进程是昂贵的,好在node在事件驱动的方式上,实现了单线程解决大并发的问题,这里启动多个进程只是为了充分将cpu资源利用起来,而不是为了解决并发的问题。服务器

1).建立子进程网络

child_process模块给予了node随意建立子进程(child_process)的能力,它提供了4个方法用于建立子进程。多线程

  1. spawn():启动一个子进程来执行命令
  2. exec():启动一个子进程来执行命令,与spawn()不一样的是使用了不一样的接口,它有一个回调函数获知子进程的情况。
  3. execFile():启动一个子进程来执行可执行文件
  4. fork():与spawn()相似,不一样点在于,它建立node的子进程只须要指定要执行的js文件模块便可。

spawn()与exec()、execFile()不一样的是,后二者建立时可指定timeout属性,设置超时时间,一旦建立的进程运行超过设定的时间进程将会被杀死。
exec()与execFile()不一样的是,exec()适合执行已有的命令,execFile()适合执行文件。这里咱们一node worker.js为例,来分别实现上述的4中方法架构

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

以上四个方法在建立子进程后,均会返回子进程对象,他们的差异以下:并发

clipboard.png

这里的可执行文件是指直接能够执行的,也就是*.exe或者.sh,若是是js文件,经过execFile()运行,那么这个文件的首行必须添加环境变量:#!/usr/bin/env node,尽管4种建立子进程的方式存在差异,可是事实上后面3种方法都是spawn()的延伸应用。

2)进程间通讯
主线程与工做线程之间经过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()传递消息。

进程间通讯原理

PC的全称是Inter-Process Communication,即进程间通讯。进程间通讯的目的是为了让不一样的进程可以互相访问资源,并进程协调工做。实现进程间通讯的技术有不少,如命名管道、匿名管道、socket、信号量、共享内存、消息队列、Domain Socket等,node中实现IPC通道的是管道技术(pipe)。

在node中管道是个抽象层面的称呼,具体细节实现由libuv提供,在win下是命名管道(named pipe)实现,在*nix下,采用unix Domain Socket来实现。

可是,具体在应用层面只是简单的message事件和send()方法,接口十分简洁和消息化。

clipboard.png

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

clipboard.png

创建链接以后的父子进程就能够自由的通讯了,因为IPC通道是用命名管道或者Domain Socket建立的,他们与网络socket的行为比较相似,属于双向通道。不一样的是他们在系统内核中就完了进程间的通讯,而不通过实际的网络层,很是高效。在node中,IPC通道被抽象为stream对象,在调用send()时发送数据(相似于write()),接收到的消息会经过message事件(相似于data)触发给应用层。

注意:只有启动的子进程是node进程是,子进程才会根据环境变量去链接IPC通道,对于其余类型的子进程则没法自动实现进程间通讯,须要让其余进程也按照约定去链接这个已经建立好的IPC通道才行。

3)句柄传递

进程间发送句柄的功能,send()方法除了可以经过IPC发送数据外还能发送句柄,第二个可选参数就是句柄:

child.send(message, [sendHandle])

句柄是一种能够用来标识资源的引用,它的内部包含了指向对象的文件描述符。所以,句柄能够用来标识一个服务端的socket对象、一个客户端的socket对象、一个udp套接字、一个管道等。
这个句柄就解决了一个问题,咱们能够去掉代理方案,在主进程接收到socket请求后,将这个socket直接发送给工做进程,而不从新与工做进程之间创建新的socket链接转发数据。咱们来看一下代码实现:

clipboard.png

主进程发送完句柄,并关闭监听以后,就变成了以下结构:

clipboard.png
这样,就能够实现多个子进程能够同时监听相同端口,再没有EADDRINUSE的异常发生。

1.句柄发送与还原
子进程对象send()方法能够发送的句柄类型包括以下几种:

  1. net.socket,tcp套接字
  2. net.Server,tcp服务器,任意创建在tcp服务上的应用层服务均可以享受到它带来的好处。
  3. net.Native,c++层面的tcp套接字或IPC管道。
  4. dgram.socket,UDP套接字
  5. 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的值和获得的文件描述符一块儿还原出一个对应的对象。这个过程的示意图以下:

clipboard.png

2.端口共同监听

3.集群稳定之路

1)进程事件
2)自动重启
3)负载均衡
4)状态共享

4.Cluster模块

1)Cluster工做原理
2)Cluster事件

2、测试

1.单元测试

2.性能测试

3、产品化

1.项目工程化

2.部署流程

3.性能

4.日志

5.监控报警

6.稳定性

7.异构共存

相关文章
相关标签/搜索