资讯专栏INFORMATION COLUMN

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

henry14 / 1401人阅读

摘要:进程间通信的目的是为了让不同的进程能够互相访问资源,并进程协调工作。这个过程的示意图如下端口共同监听集群稳定之路进程事件自动重启负载均衡状态共享模块工作原理事件二测试单元测试性能测试三产品化项目工程化部署流程性能日志监控报警稳定性异构共存

内容
9.玩转进程
10.测试
11.产品化
一、玩转进程

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

两个问题:

如何让node充分利用多核cpu服务器?

如何保证node进程的健壮性和稳定性?

1.服务模型的变迁

经历了同步(qps为1/n)、复制进程(预先赋值一定数量的进程,prefork,但是,一旦用超了,还是跟同步的服务器一样,qps为m/n)、多线程(qps为M*L/N,这种模型,当并发上万后,内存耗用的问题将会暴露出来也就是C10k问题,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
");
}).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是工作进程。这是典型的分布式架构用于并行业务处理的模式,具有较好的可伸缩性和稳定性。主进程不负责具体业务处理,只负责调度和管理工作进程,因此主进程是相对于稳定和简单的,工作进程负责具体的业务处理,因为,业务多种多样,所以,工作进程的稳定性,是我们需要考虑的。

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

1).创建子进程

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

spawn():启动一个子进程来执行命令

exec():启动一个子进程来执行命令,与spawn()不同的是使用了不同的接口,它有一个回调函数获知子进程的状况。

execFile():启动一个子进程来执行可执行文件

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");

以上四个方法在创建子进程后,均会返回子进程对象,他们的差别如下:

这里的可执行文件是指直接可以执行的,也就是*.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()方法,接口十分简洁和消息化。

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

建立连接之后的父子进程就可以自由的通信了,由于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连接转发数据。我们来看一下代码实现:

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


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

1.句柄发送与还原
子进程对象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的值和得到的文件描述符一起还原出一个对应的对象。这个过程的示意图如下:

2.端口共同监听

3.集群稳定之路

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

4.Cluster模块

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

二、测试 1.单元测试 2.性能测试 三、产品化 1.项目工程化 2.部署流程 3.性能 4.日志 5.监控报警 6.稳定性 7.异构共存

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103967.html

相关文章

  • 精读《深入浅出Node.js》

    摘要:从社区和过往的经验而言异步编程的难题已经基本解决无论是通过事件还是通过模式或者流程控制库。本章主要介绍了主流的几种异步编程解决方案这是目前中主要使用的方案。最后因为人们总是习惯性地以线性的方式进行思考以致异步编程相对较为难以掌握。 前言 如果你想要深入学习Node,那你不能错过《深入浅出Node.js》这本书,它从不同的视角介绍了 Node 内在的特点和结构。由首章Node 介绍为索引...

    codergarden 评论0 收藏0
  • 前端资源系列(4)-前端学习资源分享&前端面试资源汇总

    摘要:特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 特意对前端学习资源做一个汇总,方便自己学习查阅参考,和好友们共同进步。 本以为自己收藏的站点多,可以很快搞定,没想到一入汇总深似海。还有很多不足&遗漏的地方,欢迎补充。有错误的地方,还请斧正... 托管: welcome to git,欢迎交流,感谢star 有好友反应和斧正,会及时更新,平时业务工作时也会不定期更...

    princekin 评论0 收藏0
  • 教你如何打好根基快速入手react,vue,node

    摘要:谨记,请勿犯这样的错误。由于在之前的教程中,积累了坚实的基础。其实,这是有缘由的其复杂度在早期的学习过程中,将会带来灾难性的影响。该如何应对对于来说,虽然有大量的学习计划需要采取,且有大量的东西需要学习。 前言倘若你正在建造一间房子,那么为了能快点完成,你是否会跳过建造过程中的部分步骤?如在具体建设前先铺设好部分石头?或直接在一块裸露的土地上先建立起墙面? 又假如你是在堆砌一个结婚蛋糕...

    ddongjian0000 评论0 收藏0

发表评论

0条评论

henry14

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<